midi文件解析

midi文件说明

标准MIDI文件(SMF)是专门用来存储音序器录制和播放(不管是音序器是软件或硬件)中的数据文件格式。

此格式存储标准MIDI消息(即带有适当数据字节的状态字节)以及每个消息的时间戳(即表示“播放”事件之前要等待多少个时钟脉冲的一系列字节)。该格式可以保存以下信息:速度,每四分音符分辨率的脉冲数(或以秒为单位表示的分辨率,即SMPTE设置),时间和键签名以及音轨和样式的名称。它可以存储多个模式和轨道,以便任何应用程序在加载文件时都可以保留这些结构。

注:轨道通常是类似于一个音乐的一部分,如一个小号一部分。甲 图案将是类似于所有音乐的部分(即,小号,鼓,钢琴等)对一首歌曲或歌曲的摘录。

该格式设计为通用格式,因此任何定序器都可以读取或写入此类文件而不会丢失最重要的数据,并且足够灵活,以使特定应用程序可以以其他应用程序获胜的方式存储自己的专有“额外”数据加载文件时不要感到困惑,可以放心地忽略它不需要的多余内容。可以将MIDI文件格式视为ASCII文本文件的音乐版本(除了MIDI文件也包含二进制数据),以及将各种音序器程序作为文本编辑器都可以读取该文件。但是,与ASCII不同,MIDI文件格式可将数据分保存(即字节组,后面带有ID和大小),可以对其进行解析,加载,跳过等操作。因此,可以轻松扩展它以包括程序的专有信息。例如,某个程序可能想要保存一个“标志字节”,以指示用户是否已打开节拍器的声音。程序可以将该标志字节放入MIDI文件中,以便其他应用程序可以跳过此字节而不必了解该字节是什么意思。将来,MIDI文件格式也可以扩展为包括新的“正式”数据块,所有音序器程序都可以选择加载和使用。这可以在不使旧数据文件过时的情况下完成(即,格式被设计为以向后兼容的方式可扩展)。

什么是块?

数据总是保存在一个块中MIDI文件中可以有很多块。每个块可以具有不同的大小(即,大小是指块中包含多少个字节)。块中的数据字节以某种方式相关。块只是一组相关的字节。

每个块均以4个字符(即4个ascii字节)的ID开头,该ID指示该块是什么“类型”。接下来的4个字节(所有字节均由8位组成)形成了块的32位长度(即大小)。所有块必须以这两个字段(即8个字节)开头,这两个字段称为“ 块头”

注:长度不包括8字节块头。它只是告诉您此标头后面的块中有多少字节数据

这是一个示例标头(字节以十六进制表示):

4D 54 68 64 00 00 00 06

请注意,前4个字节构成了MThd的ascii ID (即,前4个字节是“ M”,“ T”,“ h”和“ d”的ascii值)。接下来的4个字节告诉我们,该块中应该还有6个数据字节(此后,我们应该找到下一个块头或文件末尾)。

实际上,所有MIDI文件都以该MThd标头开头(这就是您知道它是MIDI文件的方式)。

注意:组成长度的4个字节以Motorola 68000字节顺序存储,而不是以Intel反向字节顺序存储(即06是第四个字节,而不是四个的第一个字节)。MIDI文件中的所有多字节字段都遵循此标准,通常称为“大尾数”形式。

MThd块

MThd标头的ID为MThd,长度为6

让我们检查MThd块中的6个数据字节(紧随上述8个字节的头之后)。

前两个数据字节告诉Format(我更喜欢将其称为Type)。MIDI文件实际上有3种不同类型(即格式)。0类型表示文件包含一个可能包含所有16个Midi通道上的midi数据的单个轨道。如果您的音序器将其所有midi数据按照“播放”的顺序排序/存储在一个内存块中,则它应该读/写此类型。类型1表示文件包含一个或多个同时(即,全部从假定的时间0开始)轨道,也许每个轨道都在单个midi通道上。所有这些轨道一起被认为是一个序列或模式。如果您的音序器将其midi数据(即音轨)分离到不同的内存块中,但同时播放(即,作为一个“模式”),它将读取/写入此类型。类型2表示文件包含一个或多个顺序独立的单轨道模式。如果您的音序器将其Midi数据分成不同的内存块,但一次只播放一个块(即,每个块被视为不同的“摘录”或“歌曲”),则它将读取/写入此类型。

接下来的2个字节表明文件NumTracks中存储了多少个轨道当然,对于格式类型0,它始终为1。对于其他两种类型,可以有许多轨道。

最后两个字节表示多少个脉冲(即时钟)每季度注(简称PPQN)的时间戳是基于,分辨率例如,如果您的音序器具有96 ppqn,则此字段为(十六进制):

00 60

或者,如果“除法”的第一个字节为负,则表示时间戳所基于的秒数的划分。第一个字节为-24,-25,-29或-30,对应于表示每秒帧数的4个SMPTE标准。第二字节(正数)是帧(即子帧)内的分辨率。典型值可能是4(MIDI时间码),8、10、80(SMPTE位分辨率)或100。

您可以通过-25和40个子帧的数据字节指定基于毫秒的时序。

这是一个完整的MThd块(带有标头)的示例:

4D 54 68 64 MThd ID
00 00 00 06 MThd块的长度始终为6。
00 01格式类型为1。
00 02该文件中有2个MTrk块。
E7 28增量时间的每个增量代表毫秒。

MTrk块

在MThd块之后,您应该找到MTrk块,因为这是当前唯一定义的其他块。(如果找到其他一些块ID,则该ID必须是其他程序专有的,因此可以通过忽略以下由块的Length指示的数据字节来跳过它)。

MTrk块包含所有midi数据(带有定时字节),以及一个磁道的可选非midi数据显然,您在文件中遇到的MTrk块应该与MThd块的NumTracks字段指示的一样多。

MTrk标头以MTrk的ID 开头,后跟长度(即,为该磁道读取的数据字节数)。每个曲目的长度可能会有所不同。(毕竟,包含一个巴赫协奏曲小提琴声部的音轨可能比包含一个简单的2 bar鼓拍的音轨包含更多的数据)。

可变长度数量-事件的时间

以与通常在音序器中考虑音轨相同的方式来考虑MIDI文件中的音轨。音序器轨道包含一系列事件例如,音轨中的第一个事件可能是发出中间C音符。第二个事件可能是将E吹到中间C之上。这两个事件可能都同时发生。第三个事件可能是释放中间的C音符。此事件可能在前两个事件之后发生一些音乐节拍(即,按住C中间音符几个音乐节拍)。每个事件在必须发生时都有一个“时间”,并且事件按照发生的顺序排列在内存的“块”中。

在MIDI文件中,事件的“时间”在组成该事件本身的数据字节之前。换句话说,构成事件时间戳的字节排在第一位。给定事件的时间戳是从上一个事件引用的。例如,如果第一个事件在播放开始后的4个时钟发生,则其“增量时间”为04。如果下一个事件与第一个事件同时发生,则其时间为00。因此,增量时间为事件与上一个事件之间的持续时间(以时钟为单位)。

注意:由于所有轨道均以假定的时间0开始,因此第一个事件的增量时间从0开始引用。

增量时间存储为一系列字节,称为可变长度量每个字节只有前7位是有效的(右对齐;有点像ASCII字节)。因此,如果您具有32位的增量时间,则必须将其解压缩为一系列7位字节(即,就像您要通过SYSEX消息中的midi传输它一样)。当然,取决于增量时间,您将拥有可变数量的字节。为了表明哪个是系列的最后一个字节,请清除第7位。在前面所有字节中,都设置了位#7。因此,如果增量时间在0-127之间,则可以将其表示为一个字节。允许的最大增量时间为0FFFFFFF,它转换为4个字节的可变长度。以下是增量时间作为32位值的示例,以及将其转换为可变长度量的示例:

数量可变数量
 00000000 00
 00000040 40
 0000007F 7F
 00000080 81 00
 00002000 C0 00
 00003FFF FF 7F
 00004000 81 80 00
 00100000 C0 80 00
 001FFFFF FF FF 7F
 00200000 81 80 80 00
 08000000 C0 80 80 00
 0FFFFFFF FF FF FF 7F

这是一些C例程,用于读取和写入可变长度的量,例如增量时间。使用 WriteVarLen(),您传递一个32位值(即,无符号长),它会将正确的字节序列吐出到文件中。ReadVarLen()从文件中读取一系列字节,直到达到可变长度数量的最后一个字节为止,并返回32位值。

void WriteVarLen(注册无符号长值)
{
   注册无符号长缓冲区;
   缓冲区=值&0x7F;

   而((值>> = 7))
   {
     缓冲区<< = 8;
     缓冲区| =((值&0x7F)| 0x80);
   }

   而(TRUE)
   {
      putc将(缓冲液,OUTFILE);
      如果(缓冲区&0x80)
          缓冲区>> = 8;
      其他
          打破;
   }
}


双字ReadVarLen()
{
    注册双字值;
    寄存器字节c;

    if((值= getc(infile))&0x80)
    {
       值&= 0x7F;
       {
         值=(值<< 7)+((c = getc(infile))&0x7F);
       } while(c&0x80);
    }

    返回(值);
}

注意:可变长度量的概念(即,将较大的值分解为一系列字节)与MIDI文件中的delta-times之外的其他字段一起使用,这将在后面介绍。

活动

MTrk中的第一个(1到4个)字节将是第一个事件的变化时间,作为可变长度量。下一个数据字节实际上是该事件本身的第一个字节。我将其称为事件的状态对于MIDI事件,这将是实际的MIDI状态字节(如果是运行状态,则为第一个midi数据字节)。例如,如果字节为十六进制90,则此事件为Midi通道0上Note-On。例如,如果字节为十六进制23,则必须重新调用上一个事件的状态(即,midi运行状态) 。显然,MTrk中的第一个MIDI事件必须有一个状态字节。MIDI状态字节到达其1或2个数据字节后(取决于状态-某些MIDI消息仅具有1个后续数据字节)。之后,您将找到下一个事件的变化时间(作为可变数量),并开始读取下一个事件的过程。

SYSEX(系统专用)事件(状态= F0)是一种特殊情况,因为SYSEX事件可以是任意长度。在F0状态(始终存储-这里没有运行状态)之后,您会发现另一系列的可变长度字节。将它们与ReadVarLen()结合使用,您将得出一个32位的值,该值告诉您随后又有多少个字节组成了SYSEX事件。该长度不包括F0状态。

例如,考虑以下SYSEX MIDI消息:

F0 7F 7F 04 01 7F 7F F7

这将作为以下一系列字节(减去前面的增量时间字节)存储在MIDI文件中:

F0 07 7F 7F 04 01 7F 7F F7

一些Midi单元将系统专用消息作为一系列小的“数据包”发送(每个数据包传输之间存在时间延迟)。第一个数据包以F0开始,但不以F7结尾。后续数据包不以F0开头,也不以F7结尾。最后一个数据包不是以F0开头,而是以F7结尾。因此,在第一个数据包的开头F0和最后一个数据包的结尾F7之间,有一个SYSEX消息。(注意:只有极差的设计,例如卡西欧(Casio)销售的废话才能表现出这些像差)。当然,由于每个数据包之间都需要延迟,因此您需要将每个数据包存储为一个单独的事件,并将其自身的时间存储在MTrk中。另外,您需要某种方式来了解哪些事件不应以F0开头(即,除了第一个数据包以外的所有事件)。所以,MIDI文件规范重新定义了Midi状态F7(通常用作SYSEX数据包的结束标记),以指示不以F0开头的事件。如果此类事件在F0事件之后,则假定F7事件是系列的第二个“数据包”。在这种情况下,它称为SYSEX CONTINUATION事件。就像F0类型的事件一样,它具有可变的长度,后跟数据字节。另一方面,F7事件可用于存储MIDI REALTIME或MIDI COMMON消息。在这种情况下,在可变长度字节之后,您应该期望找到MIDI状态字节F1,F2,F3,F6,F8,FA,FB,FC或FE。(请注意,在SYSEX CONTINUATION事件中找不到任何此类字节)。当以这种方式使用时,F7事件称为ESCAPED事件。

FF状态被保留以指示特殊的非MIDI事件。(请注意,在MIDI中使用FF表示“重置”,因此将其存储在数据文件中并没有太大用处。因此,MIDI文件规范会随意重新定义此状态的使用)。FF状态字节之后的另一个字节告诉您它是什么类型的非MIDI事件。有点像第二个状态字节。然后,在此字节之后是另一个字节(再次是-可变长度量),该字节告诉在此事件中跟随的数据字节数(即其长度)。该长度不包括FF,类型字节或长度字节。这些特殊的非MIDI事件称为元事件,除非另有说明,否则大多数都是可选的。以下是一些已定义的元事件(包括FF状态和长度)。请注意,除非另有说明,否则在任何增量时间都可以将多个事件中的一个以上放置在Mtrk中(甚至是相同的元事件)。(就像所有Midi事件一样,Meta-Event与上一个事件相比有一个增量时间,而不管可能是哪种类型的事件。因此,您可以自由地混合MIDI和Meta事件。)

序列号

FF 00 02 SS SS

此可选事件必须在MTrk的开头(即,在任何非零增量时间之前和在任何Midi事件之前)发生,以指定序列号。这两个数据字节 ss ss是与MIDI Cue消息相对应的数字在格式2的MIDI文件中,此数字标识每个“模式”(即Mtrk),以便“歌曲”序列可以使用MIDI Cue消息引用模式。如果ss ss省略数字(即,长度字节= 0而不是2),然后使用文件中MTrk的位置(即,第一个MTrk块是第一个模式)。在仅包含一个“模式”的格式0或1中(即使格式1包含多个MTrk),此事件也仅放置在第一个MTrk中。因此,一组具有不同序号的格式1文件可以包含一个“歌曲集合”。

在格式2中,每个MTrk块中只能有一个这些事件。在格式0或1中只能有一个这些事件,它必须在第一个MTrk中。

文本

FF 01 len文字

任何目的的任何数量的文本(字节数= len)。最好将此事件放在MTrk的开头。尽管此文本可以用于任何目的,但是还有其他基于文本的元事件,例如编排,歌词,音轨名称等。此事件主要用于向程序添加的“ MIDI”文件中添加“注释”加载该文件时应该忽略它。

请注意,len可以是一系列字节,因为它表示为可变长度量。

版权

FF 02 len文字

版权信息(即文本)。最好将此事件放在MTrk的开头。

请注意,len可以是一系列字节,因为它表示为可变长度量。

序列/曲目名称

FF 03 len文字

序列或音轨的名称(即文本)。最好将此事件放在MTrk的开头。

请注意,len可以是一系列字节,因为它表示为可变长度量。

仪器

FF 04 len文字

轨道演奏的乐器的名称(即文本)。这可能与“序列/曲目名称”不同。例如,您的音序名称(即Mtrk)可能是“ Butterfly”,但是由于音轨是在钢琴上演奏的,因此您可能还需要在乐器名称中加上“ Piano”。

最好将此事件中的一个(或多个)放在MTrk的开头,以便向用户标识正在演奏曲目的乐器。通常,通过MTrk内的MIDI程序更改事件(尤其是用于常规MIDI声音模块的 MIDI文件)在音频设备上设置乐器(例如,补丁,音调,库等)因此,存在此事件仅仅是为了向用户提供轨道仪器的视觉反馈。

请注意,len可以是一系列字节,因为它表示为可变长度量。

歌词

FF 05 Len文字

在给定节拍上出现的歌曲歌词(即文本)。单个Lyric MetaEvent应该只包含一个音节。

请注意,len可以是一系列字节,因为它表示为可变长度量。

标记

FF 06 Len文字

在给定节拍上出现的标记(即文本)。标记事件可用于表示循环开始和循环结束(即,序列循环回到上一个事件)。

请注意,len可以是一系列字节,因为它表示为可变长度量。

提示点

FF 07 Len文字

在给定节拍上出现的提示点(即文本)。提示点可能用于表示WAVE(即采样声音)文件开始播放的位置,文本将是WAVE的文件名。

请注意,len可以是一系列字节,因为它表示为可变长度量。

MIDI通道

FF 20 01 cc

此可选事件通常发生在MTrk的开头(即,在任何非零增量时间之前以及在除序列号之外的任何MetaEvents之前),指定任何后续MetaEvent或System Exclusive事件与哪个MIDI通道相关联。数据字节cc是MIDI通道,其中0是第一个通道。

MIDI规范没有为系统独占事件提供MIDI通道。MetaEvents也没有嵌入通道。创建格式0 MIDI文件时,所有系统独占和MetaEvent都进入一个轨道,因此很难将这些事件与相应的MIDI语音消息相关联。(即,例如,如果您想将MIDI通道1上的音乐声部命名为“ Flute Solo”,而将MIDI通道2上的音乐声部命名为“ Trumpet Solo”,则需要使用2个音轨名称元事件。在“格式0”文件的一个轨道中,为了区分哪个轨道名称与哪个MIDI通道相关联,您可以在“ Flute Solo”轨道名称MetaEvent之前放置一个通道号为0的MIDI通道MetaEvent,然后在“”之前放置另一个MIDI通道MetaEvent,其通道号为1

如果一条轨道需要将各种事件与各种通道相关联,则在给定轨道中有多个MIDI通道事件是可以接受的。

MIDI端口

FF 21 01页

此可选事件通常发生在MTrk的开头(即,在任何非零增量时间之前和任何Midi事件之前),指定MTrk中的MIDI事件从哪个MIDI端口(即总线)中移出。数据字节pp是端口号,其中0是系统中的第一个MIDI总线。

MIDI规范每个MIDI输入/输出(即端口,总线,插孔或用于描述单个MIDI输入/输出的硬件的任何术语)限制为16个MIDI通道。给定事件的MIDI通道号被编码为事件的状态字节的最低4位。因此,通道号始终为0到15。许多MIDI接口具有多个MIDI输入/输出总线,以便解决MIDI带宽的限制(即,允许将MIDI数据更有效地发送到/从多个外部设备接收/接收)。模块),并为音乐人提供16个以上的MIDI通道。同样,某些音序器支持不止一个用于同时输入/输出的MIDI接口。不幸的是,无法将超过16个MIDI通道编码为MIDI状态字节,因此需要一种方法来识别将在其上输出的事件,例如,第二个MIDI端口的通道1与第一个MIDI端口的通道1。此MetaEvent允许音序器识别从哪个MIDI端口发送哪些MTrk事件。MIDI端口MetaEvent之后的MIDI事件将从指定的端口发送出去。

如果一个轨道需要在轨道的某个点输出到另一个端口,则在该轨道中有多个“端口”事件是可以接受的。

赛道尽头

FF 2F 00

此事件不是可选的。它必须是每个MTrk中的最后一个事件。它用作MTrk结束的确定标记。每个MTrk仅1个。

速度

FF 51 03 tt tt tt tt

指示速度变化。tt tt tt的3个数据字节是每四分音符微秒的速度。换句话说,微秒速度值告诉您音序器的每个“四分音符”应保持多长时间。例如,如果您具有07 A1 20的3个字节,则每个四分音符的长度应为0x07A120(或500,000)微秒。

因此,MIDI文件格式将节奏表示为“每四分音符的时间量(即微秒)”。

BPM

通常,音乐家将节奏表示为“每分钟(即时间段)内的四分音符的数量”。这与MIDI文件格式表示它的方式相反。

当音乐家用拍速来指代“拍子”时,他们指的是四分音符(即,在谈论拍子时,四分音符始终是1个拍子,而与拍号无关)。是的,这会使非音符感到困惑。音乐家们认为拍号的“拍子”可能与拍子的“拍子”不是一回事,除非拍子的拍子也恰好是四分音符,否则不会这样。但这是BPM拍子的传统定义。因此,对于音乐家而言,速度始终是“每分钟发生多少个四分音符”。音乐家将这种测量称为BPM(即每分钟节拍数)。因此,速度为100 BPM意味着音乐家必须在一分钟内能够连续演奏100个稳定的四分音符。这就是“快速”的“

要将MIDI文件格式的速度(即指定每四分音符微秒数的3个字节)转换为BPM:

BPM = 60,000,000 /(tt tt tt)

例如,速度为120 BPM = 07 A1 20。

那么,为什么MIDI文件格式使用“每四分音符时间”而不是“每四分音符时间”来指定其速度?好吧,使用前者更容易指定更精确的节奏。使用BPM时,如果要允许更精细的速度分辨率,有时必须处理小数速度(例如100.3 BPM)。使用微秒表示速度可提供足够的分辨率。

同样,SMPTE是基于时间的协议(即,它基于秒,分钟和小时,而不是音乐节奏)。因此,如果将其表示为微秒,则将MIDI文件的速度与SMPTE时序关联起来会更容易。现在,许多音乐设备都使用SMPTE来同步其播放。

PPQN时钟

定序器通常使用一些内部硬件计时器来抵消稳定时间(即微秒),以生成软件“ PPQN时钟”来计算时基(部门)“滴答”。这样,事件发生的时间可以用音乐bar:beat:PPQN-tick的形式表达给音乐家,而不是从回放开始后的几微秒。请记住,音乐家总是以节拍的方式思考,而不是以秒,分钟等为单位。

如前所述,微秒节奏值告诉您音序器的每个“四分音符”应保持多长时间。从这里,您可以通过将微秒值除以MIDI文件的除法来弄清楚每个音序器的PPQN时钟应多长时间。例如,如果您的MIDI文件的Division为96 PPQN,则意味着在上述速度下,每个音序器的PPQN时钟滴答应长为500,000 / 96(或5,208.3)微秒(即,每个PPQN之间应有5,208.3微秒)为了在96 PPQN时产生120 BPM的节奏,每个四分音符中应该总是有96个这些时钟音符,每个八分音符中应该有48个音符,每个十六分之一中有24个音符,依此类推)。

请注意,您可以以任何速度设置任何时基。例如,您可以以100 BPM的速度播放96个PPQN文件,就像您可以以100 BPM的速度播放192个PPQN文件一样。您还可以使96 PPQN文件以100 BPM或120 BPM的速度播放。时基和速度是两个完全独立的数量。当然,在设置硬件计时器时(即,在每个PPQN刻度中设置多少微秒时)都需要它们。当然,在较慢的速度下,您的PPQN时钟滴答声将比在较快的速度下更长。

MIDI时钟

MIDI时钟字节是通过MIDI发送的,以便同步2个设备的播放(即,一个设备以其内部计数的当前速度生成MIDI时钟,另一设备将其播放同步到这些字节的接收)。与SMPTE帧不同,MIDI时钟字节以与音乐节奏有关的速率发送。

由于每个四分音符中有24个MIDI时钟,所以MIDI时钟的长度(即,每个MIDI时钟消息之间的时间)是微秒节拍除以24。在上面的示例中,这将是500,000 / 24或20,833.3微秒在每个MIDI时钟中。或者,您可以将此与您的时基(即PPQN时钟)相关联。如果您有96个PPQN,则意味着MIDI时钟字节必须每96/24(即4个)PPQN时钟出现一次。

SMPTE

SMPTE用秒,分钟和小时(即,非音乐家计数时间的方式)来计算时间的流逝。它还将秒分解为称为“帧”的较小单位。电影行业创建了SMPTE,他们采用了4种不同的帧速率。您可以将秒分为24、25、29或30帧。后来,音乐设备需要更高的分辨率,因此每个帧都被分解为“子帧”。

因此,SMPTE与音乐节奏没有直接关系。SMPTE时间不会随“音乐节奏”而变化。

许多设备使用SMPTE同步其播放。如果您需要与此类设备同步,则可能需要处理SMPTE时序。当然,根据传递的SMPTE子帧,您可能仍将不得不维护某种PPQN时钟,以便用户可以根据BPM调整播放速度,并可以考虑每个事件的时间。就bar:beat:tick而言。但是,由于SMPTE与音乐节奏没有直接关系,因此您必须从子帧/帧/秒/分钟/小时的传递中插值(即计算)PPQN时钟(就像我们之前从硬件计时器计算出的PPQN时钟一样)数秒)。

让我们以25个帧和40个子帧为例。如之前在“分区”的讨论中所述,这类似于基于毫秒的时序,因为您每秒有1,000个SMPTE子帧。(您每秒有25帧。每秒被分成40个子帧,因此每秒有25 * 40个子帧。请记住,每秒也有1,000毫秒)。因此,每毫秒表示已经过去了另一个子帧(反之亦然)。每次您计数40个子帧时,都会通过SMPTE帧(反之亦然)。等等。

假设您需要96 PPQN和500,000微秒的速度。考虑到使用25-40 Frame-SubFrame SMPTE时序1毫秒= 1子帧(请记住1毫秒= 1,000微秒),则每四分音符应该有500,000 / 1,000(即500)个子帧。由于每个四分音符中有96个PPQN,因此每个PPQN最终长为500/96个子帧,即5.2083毫秒(即,就像我们在上面讨论PPQN时钟时所做的那样,我们最终以5,208.3微秒的PPQN时钟滴答声结束) 。由于1毫秒= 1子帧,因此在上述速度和时基下,每个PPQN时钟滴答也等于5.2083子帧。

结论

BPM = 60,000,000 / MicroTempo 

MicrosPerPPQN = MicroTempo / TimeBase 

MicrosPerMIDIClock = MicroTempo / 24 

PPQNPerMIDIClock = TimeBase / 24 

MicrosPerSubFrame = 1000000 *帧*子帧 

SubFramesPerQuarterNote = MicroTempo /(帧* Subframes) 

SubFramesPerPPQN = SubTimes / 

BaseTimesPerPPQN = SubTimes

SMPTE偏移

FF 54 05 hr mn se fr ff

指定MTrk的SMPTE开始时间(小时,分钟,秒,帧,子帧)。它应该在MTrk的开始。不应使用MIDI时间代码中的SMPTE格式对小时进行编码 在格式1文件中,SMPTE OFFSET必须与速度映射(即第一个MTrk)一起存储,并且在任何其他MTrk中都没有意义。所述FF字段包含在一帧的100分之分数帧,即使是在基于SMPTE MTrks其指定Δ-倍(即,从在该MTHD子帧设置不同的)不同的帧细分。

时间签名

FF 58 04 nn dd cc bb

拍号用4个数字表示。nndd表示在乐谱上标注的签名的“分子”和“分母”。分母是2的负光焦度:2 =四分音符,3 =第八等。立方厘米表示MIDI时钟的节拍器的数目。BB参数表示的第32届谱写音符在MIDI四分音符(24个MIDI时钟)的数量。此事件使程序可以将MIDI的四分之一与完全不同的东西相关联。例如,每3个八分音符有6/8次节拍器声单击和每四分之一音符24个时钟将是以下事件:

FF 58 04 06 03 18 08

密钥签名

FF 59 02平方英尺mi

sf = -7对于7个单位,-1对于1个单位,以此类推,0对于c键,1对于1尖锐,依此类推。

mi = 0(主要),1(次要)

专有事件

FF 7F镜头数据...

程序可以使用它来存储专有数据。第一个字节应该是某种唯一的ID,以便程序可以标识事件是属于该事件还是属于某个其他程序。为此,建议使用4个字符(即ascii)的ID。

请注意,len可以是一系列字节,因为它表示为可变长度量。

勘误表

在格式为0的文件中,速度和拍号更改分散在一个MTrk中。在格式1中,第一个MTrk应该仅由速度和时间签名事件组成,以便可以由某些能够生成“速度图”的设备读取。在格式2中,每个MTrk应该至少以一个初始速度和拍号事件开始。

注意:如果MIDI文件中没有速度和拍子事件,则假定120 BPM和4/4。

RMID文件

将数据保存在块中的方法(即,数据之前是由4个字符ID和32位大小的字段组成的8字节头)是交换文件格式的基础。现在,您应该阅读有关交换文件格式的文章, 以获取背景信息。

如前所述,MIDI文件格式是“损坏的” IFF。文件开头缺少文件头。一件不好的事情是,一个标准的IFF解析例程将阻塞MIDI文件(因为它期望前12个字节是组ID,文件大小和类型ID字段)。为了修复MIDI文件格式,使其严格遵守IFF,Microsoft简单地组成了MIDI文件之前的12字节标头,从而提出了RMID格式。RMID文件以“ R”,“ I”,“ F”,“ F”的组ID(4个ASCII字符)开头,然后是32位文件大小字段,然后是“ R”,“中'。然后,跟随MIDI文件的块(即MThd和MTrk块)。如果您将RMID文件的前12个字节切掉,

请注意,MIDI文件中的块不会填充(额外的0字节)为偶数个字节。我不知道RMID格式是否也可以纠正MIDI文件格式的这种畸变。

格式总结

头区块 (Head Chunk)

MThd + <区块字符长度> + <数据>

头区块字符长度 一般为 6

轨道区块 (Track Chunk)

Track Chunk = MTrk + <区块字符长度> + <MIDI 事件>

区块字符长度:是 4 byte 的无符号长整型

<MIDI 事件> = <时间戳> + ( <元 (Meta) 事件> | <普通事件> | <系统 (System) 事件>)

时间戳:为用 VLQ 表示的相对于上一个数据点的时间变化量 (delta time)

<元 (Meta) 事件> = \xFF + <类型> + <数据长度> + <数据>

类型:为 1 byte 的字节,对照表在下文

数据长度:为用 VLQ 表示的数据块的字符数

例子

比如下面这段 bwv806a.mid 的前 200 字节:

MThd\x00\x00\x00\x06\x00\x01\x00\x04\x00\xf0MTrk\x00\x00\x00G\x00\xff\x03\x08untitled\x00\xffT\x05`\x00\x03\x00\x00\x00\xffX\x04\x0c\x03\x0c\x08\x00\xffY\x02\x00\x00\x00\xffQ\x03\x06EO\x83\x97h\xffQ\x03\x07\xa1 \x82h\xffQ\x03\t\xa3\x1b\x82h\xffQ\x03\x0c\xe5\x0e\x00\xff/\x00MTrk\x00\x00\n\xd9\x00\xff!\x01\x00\x00\xff\x03\x1bEnglish Suite 1, 1. Prelude\x00\xc0\x00\x00\xb0\x07d\x00\n@\x90p\x90Qk\x81pLkxIk\x82hEk`Q\x00\x18PkHL\x00\x00E\x00\x00I\x00\x18P\x00\x18Nk`N\x00\x18Lk`L\x00\x18Nk`N\x00\x18

翻译如下:

MThd
\x00\x00\x00\x06
\x00\x01    \x00\x04    \x00\xf0
0, 0, Header, 1, 4, 240

MTrk
\x00\x00\x00 G      # length of track
1, 0, Start_track

\x00    \xff    \x03    \x08    untitled
1, 0, Title_t, "untitled"              # Time0, METAEVENT, Type3, len8, data

\x00    \xff    T    \x05    `\x00\x03\x00\x00
1, 0, SMPTE_offset, 96, 0, 3, 0, 0

\x00    \xff    X    \x04    \x0c\x03\x0c\x08
1, 0, Time_signature, 12, 3, 12, 8

\x00    \xff    Y    \x02    \x00\x00
1, 0, Key_signature, 0, "major"

\x00    \xff    Q    \x03    \x06EO
1, 0, Tempo, 410959

\x83\x97 h    \xff    Q    \x03    \x07\xa1   # 注意这里结尾\xa1后还有一个空白字符 ' '
1, 52200, Tempo, 500000

\x82 h    \xff    Q    \x03    \ t \xa3
1, 52560, Tempo, 631579

\x1b\x82 h    \xff    Q    \x03    \x0c\xe5\x0e
1, 52920, Tempo, 845070

\x00    \xff    /    \x00
1, 52920, End_track

MTrk
\x00\x00 \n \xd9
2, 0, Start_track

\x00    \xff    !    \x01    \x00
2, 0, MIDI_port, 0

\x00    \xff    \x03    \x1b    English Suite 1, 1. Prelude
2, 0, Title_t, "English Suite 1, 1. Prelude"

\x00    \xc0    \x00
2, 0, Program_c, 0, 0

\x00    \xb0    \x07 d
2, 0, Control_c, 0, 7, 100

\x00    \n    @
2, 0, Control_c, 0, 10, 64

\x90 p    \x90    Q    k
2, 2160, Note_on_c, 0, 81, 107

\x81 p    LkxIk
2, 2400, Note_on_c, 0, 76, 107

\x82 h    Ek
2, 2520, Note_on_c, 0, 73, 107
.......

程序

from struct import unpack
import time

def read_vlq(f):
    result = ''
    buffer = unpack('B', f.read(1))[0]
    length = 1
    while buffer > 127:
        print(buffer)
        result += '{0:{fill}{n}b}'.format(buffer-128, fill='0', n=7)
        buffer = unpack('B', f.read(1))[0]
        length += 1

    result += '{0:{fill}{n}b}'.format(buffer, fill='0', n=7)
    return int(result, 2), length


def parse_event(evt, param):
    if 128 <= evt <= 143:
        print('Note Off event.')
    elif 144 <= evt <= 159:
        print('Note On event.', unpack('>BB', param))
    elif 176 <= evt <= 191:
        print('Control Change.')
    elif 192 <= evt <= 207:
        print('Program Change.')

with open('bwv806a.mid', 'rb') as f:
    print(f.read(200))
    # HEADER
    if f.read(4) != b'MThd':
        raise Exception('not a midi file!')
    print(f.read(4))
    header_info = f.read(6)
    print(unpack('>hhh', header_info))

    ''' ================================== '''
    while True:
        track_head = f.read(4)
        if track_head != b'MTrk':
            if track_head != b'':
                print(f.read(20))
                raise Exception('not a midi file!')
            else:
                break
        
        # length of track
        len_of_track = unpack('>L', f.read(4))[0]
        # print('len_of_track ', len_of_track)

        counter = 0
        t = 0
        last_event = None
        while True:
            delta_t, len_ = read_vlq(f)
            counter += len_
            t += delta_t
            # print('T ', t, end='')
            event_code = f.read(1)
            event_type = unpack('>B', event_code)[0]
            counter += 1
            # print(' event_type ', event_type, end='')
            if event_type == 255:
                meta_type = f.read(1)
                counter += 1
                # print(' - meta_type ', meta_type, end='')
                data_len, len_= read_vlq(f)
                counter += len_
                data = f.read(data_len)
                counter += data_len
                # print(' - ', data)
            elif event_type <= 127:
                parse_event(last_event, event_code+f.read(1))
                counter += 1
            else:
                if 128 <= event_type <= 143:
                    # print(' Note Off event.', end='')
                    parse_event(event_type, f.read(2))
                    counter += 2
                elif 144 <= event_type <= 159:
                    # print(' Note On event.', end='')
                    parse_event(event_type, f.read(2))
                    counter += 2
                elif 176 <= event_type <= 191:
                    # print(' Control Change.', end='')
                    parse_event(event_type, f.read(2))
                    counter += 2
                elif 192 <= event_type <= 207:
                    # print(' Program Change.', end='')
                    parse_event(event_type, f.read(1))
                    counter += 1
                last_event = event_type


            # print(counter)
            if counter == len_of_track:
                break

python_midi库的使用

源码地址:https://github.com/vishnubob/python-midi#Installation

import os
import numpy as np
import midi

from music21 import environment,converter
md = midi.read_midifile(r'test.mid')
# print(md[0])
# midi.write_midifile('out.mid',md)

import midi
# print(midi.G_3)
# Instantiate a MIDI Pattern (contains a list of tracks)
pattern = midi.Pattern(resolution=480)
# Instantiate a MIDI Track (contains a list of MIDI events)
track = midi.Track()
# Append the track to the pattern
pattern.append(track)
for e in md[0][:]:
    track.append(e)

off = midi.NoteOffEvent(tick=100, pitch=midi.G_3)
track.append(off)
# Add the end of track event, append it to the track
eot = midi.EndOfTrackEvent(tick=1)
track.append(eot)
# Print out the pattern
print(pattern)
# Save the pattern to disk
midi.write_midifile("output1.mid", pattern)

'''关联MuseScore3可视化'''
# us = environment.UserSettings()
# us['musescoreDirectPNGPath'] = "C:/Program Files/MuseScore 3/bin/MuseScore3.exe"
# us['musicxmlPath'] = "C:/Program Files/MuseScore 3/bin/MuseScore3.exe"
# score = converter.parseFile("output1.mid")
# score.show()
# print(environment.keys())

'播放midi文件'
import time
import pygame
pygame.mixer.init()
pygame.mixer.music.load('output1.mid')
pygame.mixer.music.play()
while True:
    time.sleep(1)
    if pygame.mixer.music.get_busy() == 0:
        break

midi音符编号表

下表按八度列出了所有MIDI音符编号。

绝对倍频数指定基于中间C = C4,这是一个任意但使用广泛的分配。

 

八度音符编号
 CC#DD#EFF#GG#AA#B
-1 0 1 2 3 4 5 6 7 8 9 10 11
0 12 13 14 15 16 17 18 19 20 21 22 23
1 24 25 26 27 28 29 30 31 32 33 34 35
2 36 37 38 39 40 41 42 43 44 45 46 47
3 48 49 50 51 52 53 54 55 56 57 58 59
4 60 61 62 63 64 65 66 67 68 69 70 71
5 72 73 74 75 76 77 78 79 80 81 82 83
6 84 85 86 87 88 89 90 91 92 93 94 95
7 96 97 98 99 100 101 102 103 104 105 106 107
8 108 109 110 111 112 113 114 115 116 117 118 119
9 120 121 122 123 124 125 126 127        

 

除通道10外,所有MIDI通道的这些声音都是相同的,通道10仅具有敲击声和一些声音“效果”。

在MIDI通道10上,每个MIDI音符编号(“ Key#”)对应于不同的鼓声,如下所示。兼容GM的乐器必须在此处显示的按键上具有声音。虽然许多当前乐器还具有此处显示的范围之上或之下的其他声音,甚至可能具有带有这些声音变体的其他“套组”,但通用MIDI仅支持这些声音。

打击乐器编号音符鼓声
打击乐器编号
音符鼓声
35 B1 Acoustic Bass Drum 59 B3 Ride Cymbal 2
36 C2 Bass Drum 1 60 C4 Hi Bongo
37 C#2 Side Stick 61 C#4 Low Bongo
38 D2 Acoustic Snare 62 D4 Mute Hi Conga
39 D#2 Hand Clap 63 D#4 Open Hi Conga
40 E2 Electric Snare 64 E4 Low Conga
41 F2 Low Floor Tom 65 F4 High Timbale
42 F#2 Closed Hi Hat 66 F#4 Low Timbale
43 G2 High Floor Tom 67 G4 High Agogo
44 G#2 Pedal Hi-Hat 68 G#4 Low Agogo
45 A2 Low Tom 69 A4 Cabasa
46 A#2 Open Hi-Hat 70 A#4 Maracas
47 B2 Low-Mid Tom 71 B4 Short Whistle
48 C3 Hi Mid Tom 72 C5 Long Whistle
49 C#3 Crash Cymbal 1 73 C#5 Short Guiro
50 D3 High Tom 74 D5 Long Guiro
51 D#3 Ride Cymbal 1 75 D#5 Claves
52 E3 Chinese Cymbal 76 E5 Hi Wood Block
53 F3 Ride Bell 77 F5 Low Wood Block
54 F#3 Tambourine 78 F#5 Mute Cuica
55 G3 Splash Cymbal 79 G5 Open Cuica
56 G#3 Cowbell 80 G#5 Mute Triangle
57 A3 Crash Cymbal 2 81 A5 Open Triangle
58 A#3 Vibraslap      

 

 

参考资料:

Outline of the Standard MIDI File Structure (英文),对 midi 文件的结构进行了解释
The Midi File Format (英文),另一篇比较好的说明文章
Standard MIDI-File Format Spec. 1.1, updated (英文),详细说明了 VLQ 的一些信息
MIDI Channel Voice Messages (英文),midi_event 详解
Python 3 struct 用法 (英文)
Variable-length_quantity (英文),一种用 Python 来 parse VLQ 量的方法

 

posted @ 2019-10-03 00:34  码迷-wjz  阅读(8186)  评论(2编辑  收藏  举报