标准MIDI文件格式
达思挺·考德威尔
标准的MIDI文件格式就像奇异的兽。总体看来,它是那样的让你无法抗拒。当然,你怎样看它无关紧要,可是用足够多的描述符描述一段音乐并使它能够重现,可不是很少的工作就可以完成的。然而,它虽然复杂,但是真正理解之后,MIDI文件格式的结构还是很直观明了的。
在这里我必须放弃一些东西,因为毕竟我不是MIDI也不是MIDI文件专家!最近我为我的PC准备了一块Gravis 超音频音效卡,利用它听完几段MIDI文件(.mid)之后,想:“呵,我要是能够制作自己的MIDI(.mid)文件该多好啊!”嗯,经过烦人的几个小时之后,我发现,那些并不是没有价值的工作。但是,我是不会让一个冗长的文件格式就能够阻止的(此外,我告诉过我的妻子,计算机不是很难用的,而且我十分憎恨当一个伪君子)。那么,在这篇文章中如果发现什么错误,请让我知道,我会修改它的。同时,这份文档的范围并没有提供所有类型的MIDI命令和任何可能的文件配置!这篇基本指南将使读者能够(以中等的时间投资)制作出MIDI类型的文件。
1.概述:
一个MIDI文件基本上由两个部分组成,头块和轨道块。第二节讲述头块,第三节讲述轨道块。一个MIDI文件有一个头块用来描述文件的格式、许多的轨道块等内容。一个轨道可以想象为像一个大型多音轨录音机那样,你可以为某种声音、某种乐谱、某种乐器或者你需要的任何东西分配一个轨道。
2.头块:
头块出现在文件的开头,有三种方式来描述文件。头块看起来一直是这样的:
4D 54 68 64 00 00 00 06 ff ff nn nn dd dd
前4个字节等同于ASCII码MThd,接着MThd之后的4个字节是头的大小。它将一直是00 00 00 00 06,因为现行的头信息将一直是6字节。
ff ff是文件的格式,有3种格式:
0-单轨
1-多规,同步
2-多规,异步
单轨,很显然就只有一个轨道。同步多轨意味着所有轨道都是垂直同步的,或者其他的措辞为他们都在同一时间开始,并且可以表现一首歌的不同部分。异步多轨没有必要同时开始,而且可以完全的不同步。
nn nn 是MIDI文件中的轨道数。
dd dd 是每个4分音符delta-time节奏数(这之后将做详细介绍)。
3.轨道块:
头块之后剩下的文件部分是轨道块。每一个轨道包含一个头,并且可以包含你所希望的许多MIDI命令。轨道头与文件头及其相似:
4D 54 72 6B xx xx xx xx
与头一致,前4个字节是ASCII吗,这个是MTrk,紧跟MTrk的4个字节给出了以字节为单位的轨道的长度(不包括轨道头)。
在头之下是MIDI事件,这些事件同现行的可以被带有累加的MIDI合成器端口接受和发送的数据是相同的。一个MIDI 事件先于一个delta-time。一个delta-time是一个MIDI事件被执行后的节奏数,每个四分之一音符的节奏数先前已经定义在了文件的头块中。这个delta-time是一个可变长度的编码值。这种格式虽然混乱,可是允许根据需要利用多位表示较大的数值,这不会因为需求小的数值情况下以添零的方式浪费掉一些字节!数值被转换为7位的字节,并且除了最后一个字节以最高有效位是0外,各个字节最有意义的一位是1,。这就允许一个数值被一次一个字节地读取,你如果发现最高有效位是0,则这就是这个数值的最后一位(意义比较小)。依照MIDI说明,全部delta-time的长度最多超过4字节。
delta-time 之后就是MIDI事件,每个MIDI事件(除了正在运行的事件外)带有一个最高有效位总是1的命令字节(值将>128)。大部分命令的列表在附录A中。每个命令都有不同的参数和长度,但是接下来的数据将是最高有效位为零(值将<128)。这里有个例外就是meta-event,最高有效位可以是1。然而,meta-events需要一个长的参数以区分。
微小失误就可以导致混乱的是运行模式,这是现行MIDI命令所忽略的地方,并且最终发行的MIDI命令是假定的。这就意味这如果包含了命令,那么MIDI事件就是由delta-time与参数组成而转换的。
4.综述:
如果这份说明仅仅是使问题更加混乱,那么以下提供的例子可能有助于澄清问题!同时,两个公用程序和一个图解文件包含在这个文档里面:
DEC.EXE——这个公共程序是将二进制文件(比如.MID)转换成以十进制表示的对应每个字节的有标记界限的文本文件。
REC.EXE——这个公共程序是将有标记界限的十进制数文本文件对应的每一字节转换成二进制文件。
MIDINOTE.PS——这是一个对应键盘和五线谱的音符数字附录页。
附录A
1.MIDI事件命令
每个命令字节有两部分,左nybble(4位)包含现行的命令,右nybble包含将被执行的命令的通道号,这里有16各MIDI通道8个MIDI命令(命令nybble必须最高有效位是1的)。在下表中,X表示MIDI通道号。所有的音符即数据字节都<128(最高有效位是0)。
十六进制 二进制 数据 描述
8x 1000xxxx nn vv 音符关闭 (释放键盘)
nn=音符号
vv=速度
9x 1001xxxx nn vv 音符打开 (按下键盘)
nn=音符号
vv=速度
Ax 1010xxxx nn vv 触摸键盘以后
nn=音符号
vv=速度
Bx 1011xxxx cc vv 调换控制
cc=控制号
vv=新值
Cx 1100xxxx pp 改变程序(片断)
pp=新的程序号
Dx 1101xxxx cc 在通道后接触
cc=管道号
Ex 1110xxxx bb tt 改变互相咬和的齿轮 (2000H 表明缺省或没有改变)(什么意思搞不懂:)
bb=值的低7位(least sig)
tt=值的高7位 (most sig)
下表是没有通道的 meta-events列表 ,他们的格式是:
FF xx nn dd
所有的 meta-events 是以 FF 开头的命令 (xx),长度,或者含在数据的字节数(nn),现行的数据(dd)
十六进制 二进制 数据 描述
00 00000000 nn ssss 设定轨道的序号
nn=02 (两字节长度的序号)
ssss=序号
01 00000001 nn tt .. 你需要的所有文本事件
nn=以字节为单位的文本长度
tt=文本字符
02 00000010 nn tt .. 同文本的事件, 但是用于版权信息
nn tt=同文本事件
03 00000011 nn tt .. 序列或者轨道名
nn tt=同文本事件
04 00000100 nn tt .. 轨道乐器名
nn tt=同文本事件
05 00000101 nn tt .. 歌词
nn tt=同文本事件
06 00000110 nn tt .. 标签
nn tt=同文本事件
07 00000111 nn tt .. 浮点音符
nn tt=同文本事件
2F 00101111 00 这个事件一定在每个轨道的结尾出现
51 01010001 03 tttttt 设定拍子
tttttt=微秒/四分音符
58 01011000 04 nn dd cc bb 拍子记号
nn=拍子记号分子
dd=拍子记号分母2=四分之一
3=8分拍, 等等.
cc=节拍器的节奏
bb=对四分之一音符标注的第32号数字
59 01011001 02 sf mi 音调符号
sf=升调/降调(-7=7 降调, 0=基准C调,7=7 升调)
mi=大调/小调(0=大调, 1=小调)
7F 01111111 xx dd .. 音序器的详细信息
xx=被发送的字节数
dd=数据
下表列出了控制整个系统的系统消息。这里没有MIDI通道数 (这些一般仅应用于MIDI键盘等.)
十六进制 二进制 数据 描述
F8 11111000 同步所必须的计时器
FA 11111010 开始当前的队列
FB 11111011 从停止的地方继续一个队列
FC 11111100 停止一个队列
下表列出的是与音符相对应的命令标记。
八度音阶¦¦ 音符号
# ¦¦
¦¦ C ¦ C# ¦ D ¦ D# ¦ E ¦ F ¦ F# ¦ G ¦ G# ¦ A ¦ A# ¦ B
-----------------------------------------------------------------------------
0 ¦¦ 0 ¦ 1 ¦ 2 ¦ 3 ¦ 4 ¦ 5 ¦ 6 ¦ 7 ¦ 8 ¦ 9 ¦ 10 ¦ 11
1 ¦¦ 12 ¦ 13 ¦ 14 ¦ 15 ¦ 16 ¦ 17 ¦ 18 ¦ 19 ¦ 20 ¦ 21 ¦ 22 ¦ 23
2 ¦¦ 24 ¦ 25 ¦ 26 ¦ 27 ¦ 28 ¦ 29 ¦ 30 ¦ 31 ¦ 32 ¦ 33 ¦ 34 ¦ 35
3 ¦¦ 36 ¦ 37 ¦ 38 ¦ 39 ¦ 40 ¦ 41 ¦ 42 ¦ 43 ¦ 44 ¦ 45 ¦ 46 ¦ 47
4 ¦¦ 48 ¦ 49 ¦ 50 ¦ 51 ¦ 52 ¦ 53 ¦ 54 ¦ 55 ¦ 56 ¦ 57 ¦ 58 ¦ 59
5 ¦¦ 60 ¦ 61 ¦ 62 ¦ 63 ¦ 64 ¦ 65 ¦ 66 ¦ 67 ¦ 68 ¦ 69 ¦ 70 ¦ 71
6 ¦¦ 72 ¦ 73 ¦ 74 ¦ 75 ¦ 76 ¦ 77 ¦ 78 ¦ 79 ¦ 80 ¦ 81 ¦ 82 ¦ 83
7 ¦¦ 84 ¦ 85 ¦ 86 ¦ 87 ¦ 88 ¦ 89 ¦ 90 ¦ 91 ¦ 92 ¦ 93 ¦ 94 ¦ 95
8 ¦¦ 96 ¦ 97 ¦ 98 ¦ 99 ¦ 100 ¦ 101 ¦ 102 ¦ 103 ¦ 104 ¦ 105 ¦ 106 ¦ 107
9 ¦¦ 108 ¦ 109 ¦ 110 ¦ 111 ¦ 112 ¦ 113 ¦ 114 ¦ 115 ¦ 116 ¦ 117 ¦ 118 ¦ 119
10 ¦¦ 120 ¦ 121 ¦ 122 ¦ 123 ¦ 124 ¦ 125 ¦ 126 ¦ 127 ¦
参考资料:
"MIDI Systems and Control" Francis Rumsey 1990 Focal Press
"MIDI and Sound Book for the Atari ST" Bernd Enders and Wolfgang Klem 1989 M&T Publishing, Inc.
MIDI file specs and general MIDI specs were also obtained by sending e-mail to LISTSERV@AUVM.AMERICAN.EDU with the phrase GET MIDISPEC PACKAGE in the message.
------------------------------ DEC.CPP ------------------------------------
/* file dec.cpp
by Dustin Caldwell (dustin@gse.utah.edu)
*/
#include <dos.h>
#include <stdio.h>
#include <stdlib.h>
void helpdoc();
main()
{
FILE *fp;
unsigned char ch, c;
if((fp=fopen(_argv[1], "rb"))==NULL) /* open file to read */
{
printf("cannot open file %s/n",_argv[1]);
helpdoc();
exit(-1);
}
c=0;
ch=fgetc(fp);
while(!feof(fp)) /* loop for whole file */
{
printf("%u/t", ch); /* print every byte's decimal equiv. */
c++;
if(c>8) /* print 8 numbers to a line */
{
c=0;
printf("/n");
}
ch=fgetc(fp);
}
fclose(fp); /* close up */
}
void helpdoc() /* print help message */
{
printf("/n Binary File Decoder/n/n");
printf("/n Syntax: dec binary_file_name/n/n");
printf("by Dustin Caldwell (dustin@gse.utah.edu)/n/n");
printf("This is a filter program that reads a binary file/n");
printf("and prints the decimal equivalent of each byte/n");
printf("tab-separated. This is mostly useful when piped /n");
printf("into another file to be edited manually. eg:/n/n");
printf("c:/>dec sonata3.mid > son3.txt/n/n");
printf("This will create a file called son3.txt which can/n");
printf("be edited with any ascii editor. /n/n");
printf("(rec.exe may also be useful, as it reencodes the /n");
printf("ascii text file)./n/n");
printf("Have Fun!!/n");
}
---------------------------- REC.CPP ----------------------------------
/* File rec.cpp
by Dustin Caldwell (dustin@gse.utah.edu)
*/
#include <dos.h>
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
void helpdoc();
main()
{
FILE *rfp, *wfp;
unsigned char ch, c;
char s[20];
if((rfp=fopen(_argv[1], "r"))==NULL) /* open the read file */
{
printf("cannot open file %s /n",_argv[1]);
helpdoc();
exit(-1);
}
if((wfp=fopen(_argv[2], "wb"))==NULL) /* open the write file */
{
printf("cannot open file %s /n",_argv[1]);
helpdoc();
exit(-1);
}
c=0;
ch=fgetc(rfp);
while(!feof(rfp)) /* loop for whole file */
{
if(isalnum(ch)) /* only 'see' valid ascii chars */
{
c=0;
while(isdigit(ch)) /* only use decimal digits (0-9) */
{
s[c]=ch; /* build a string containing the number */
c++;
ch=fgetc(rfp);
}
s[c]=NULL; /* must have NULL terminator */
fputc(atoi(s), wfp);/* write the binary equivalent to file */
}
ch=fgetc(rfp); /* loop until next number starts */
}
fclose(rfp); /* close up */
fclose(wfp);
}
void helpdoc() /* print help message */
{
printf("/n Text File Encoder/n/n");
printf("/n Syntax: rec text_file_name binary_file_name/n/n");
printf("by Dustin Caldwell (dustin@gse.utah.edu)/n/n");
printf("This is a program that reads an ascii tab-/n");
printf("delimited file and builds a binary file where/n");
printf("each byte of the binary file is one of the decimal/n");
printf("digits in the text file./n");
printf(" eg:/n/n");
printf("c:/>rec son3.txt son3.mid/n/n");
printf("(This will create a file called son3.mid which is/n");
printf("a valid binary file)/n/n");
printf("(dec.exe may also be useful, as it decodes binary files)/n/n");
printf("Have Fun!!/n");
}