随便说点啥
不想看废话的、查询GM标准音色表、MIDI信息状态表等资料的同僚们请 直奔附录 。眼看2013就结束了,想着用啥来总结好呢。2013大半时间在路上,有空了小旅馆里开着网想一想,还是蛮有意思的一个东西。HTML5火爆了2013,图像、游戏、交互等等各个分支发展都很喜人。走马观花溜一遍,到了数字音乐领域,感觉有点像看春哥和曾哥对台唱歌一样从繁华喧闹的市集走到胡同脚里一片黯然收场。零零散散搜集到一些资料,算是给自己的2013一个交代。想做的事情越来越多,时间越来越少——希望自己2014年能在感兴趣的方面有一些收获。
其实之前也跟一些朋友聊起过这个话题,让我比较诧异的是MIDI这东西在很多人眼里还一直是极客手中的玩物——也难怪,这应当归咎于数字音乐在互联网的缓步发展。回顾2013年,有许多令人尖叫的音乐相关作品问世,例如google的 吉他doodle , 多人游戏plink 等等无不令人啧啧称赞。
社交化的数字音乐 仍然一如既往拥有很大的市场,受限于技术门槛和不完善的标准支持,这方面国内国外都鲜有建树,即便桌面领域或者基于flash的应用已经相当成熟——这是后话了。
受限于鄙人学识,以下内容部分纯属胡编滥造,如有误导烦请指正,或者你来打我啊。
虚拟MIDI环境简介
传统MIDI一系列设备,没有几年功夫和一定的存款,要想搭建一套可以自己谱谱曲的环境,不用烂几块键盘没几个几十G的雅马哈音色库恐怕很难搞定。许多专业录音棚出产的电子乐甚至可以媲美中端音乐厅水平。当然吾等屌丝不需要那么豪华的阵容,硬盘加右手就够了。设想在一个只有浏览器的环境中创建可以实时播放的MIDI环境。最基本的,这套工具应该可以方便的让我这样不懂乐理的人也会点出声加点特效,钢琴二胡和隔壁王老坎家骡子拉磨的声音来个混响,最后还能允许我把这声音弄下来挂门上驱灾撵狗啥的。
为了实现这个简单的目的,我们需要一些基本的道具,包括可以敲的 输入设备 ,用于混响和播放声音的 通道 ,能够管理通道信息和状态的 合成器 ,装了各种声音的 音色库(软波表) ,其中应当有我想要的骡子声 乐器 ,一个正常的喇叭作为 输出设备 ,最后把这些所有东西集合起来可以打包输出文件的——几年前我们一伙人管这叫—— 数字音乐模拟系统 ,至今我仍然认为这是个好名字。
MIDI
MIDI实际上只是一个乐谱,只不过五线谱是给演奏家看的,MIDI是给合成器看的。MIDI文件中定义了在某一时刻用某种节奏的某个音量播放某种乐器,持续多长时间采用什么效果。除此之外还能记录诸如“幕布升起”、“扣钉骑士万岁万岁万万岁”等类似的文本信息和提示内容,提供给读乐谱的程序看。输入设备
不赘述,我们有键盘,上百个键位和控制键,完全够用了。可能什么键对应啥操作需要好好想一想。通道/Channel
通道是一系列互不干扰的音频流传输介质。MIDI标准中允许一个合成器同时管理16个通道,一个通道同一时间只能维护一种乐器状态。不过理论上来说一个MIDI乐谱中可以同时存在的乐器数无上限的,只是在超过通道限制时需要在乐谱中告诉合成器某某时刻应该切换乐器,合适的时候再切换回去云云。在非真实MIDI硬件设备的模拟环境中尤其对于浏览器环境而言,需要手动实现对于音频流的管理。合成器/Synthesizer
简单来说就是一个音频流的管理程序,提供解读乐谱、混响、音效支持和通道控制等功能。合成器通过读取给定的音色库解析和播放MIDI乐谱。对于非人民币玩家,要想在浏览器环境中以原生的方式搞到这个神奇的东西,用代码写吧XD。好在目前HTML5中的 AudioAPI 提供的一系列音频流处理接口已经基本满足构造一个能跑的合成器的条件。音色库/Sound Font/Sound Bank
我们所讨论的软波表,特指装载了一系列乐器音效的一个打包文件。该文件中存储了一系列基本的压缩音频采样,并定义了一套规范用于描述特定的乐器在给定 音调/Tone 和 音量/Voice 时应当被如何通过已有的采样信息进行混响。解析并执行这一套规范就是合成器的作用。广泛使用的音色库文件如 .sf2 ,几乎已成为MIDI音色库现行标准。好的音色库动辄十几张CD的存储量,这么大的容量得益于其未经压缩的采样率。雅马哈的一些特效音色库制作甚至耗时几年,从亚马逊河流域、阿尔卑斯山脉等地方录下最纯粹和原始的自然声音压制而成。上面这句纯属忽悠。目前几乎所有的操作系统中默认装载了声卡里的音色库,例如WIndows下经典的"GS波表软件合成器"——这便是为何可以直接在windows中点击播放MIDI文件的原因——一个约3.4M大小带音色库的合成器,支持 GS/GM标准 ,该音色库存在"C:\Windows\System32\drivers\gm.dls"。好的声卡显而易见会提供更好的音色库,以便专业玩家创造出众的音乐体验。
音色库文件 的解析,在HTML5规范中几乎是一片空白。不过国外有研究团队已经在这方面颇有建树, HTML5 MIDI API 的贡献者之一, 日本哥Toyoshim 的开源项目 T'Sound System Library 已经实现 用js读取.sf2格式文件 。著名的HTML5音频研究团队的MIDI框架 MIDI.js 用了一套已经打包为 .mp3 格式的base64文件来模拟音色库,这也是目前几乎所有web音频类应用的通用做法并且仍然没有更好的策略。即便是大名鼎鼎的Java,对于音色库的支持也一直推迟到 JDK7的com.sun.media.sound 包中才得以完整实现。
乐器/Instrument
存储在音色库中的采样音频以及对该音频的预定义播放方式,展现在输出设备中就是乐器。GM音色库标准中定义了 128种标准乐器 ,但不同的音色库中包含的音色信息可能相差非常大,这也是为何 .midi 格式的文件在不同的系统下可能会播放出不同效果的原因。正常情况下,通道 K 默认对应除了标准128中乐器中的第 K 个乐器,除非合成器告诉他需要切换乐器;特殊情况为作为打击乐保留通道的第Channel 10,其中的正常音符在默认情况下可能表现出不同的音色。
输出设备
声卡和音响设备。但凡是个电脑都有的东西,并且浏览器已经通过AudioApi封装了对输出设备的访问,所以不需要我们关心。小霸王至尊8合1
事情到目前为止进展的似乎挺不错。我们只需要提供一个可以操作的界面,把这些东西全部打包装在一起,这就是个可以在浏览器端生成MIDI乐谱、可以导出为MP3的超级8合1至尊卡带了。不过等等,MIDI乐谱?MIDI文件结构
看来我们遇到了一点小麻烦,在解决如何生成MIDI乐谱之前可能得复习复习MIDI文件结构。关于这方面已经有诸多文献可参考,例如鄙人本科时与好基友 大树阴凉儿 所著的 Bottom-up Digital Music Simulation System Under Program Control (百度貌似也能搜到免费版。。。)。这里有张图简要概括了MIDI文件结构:
音序/Sequence
我们称一个完整的MIDI乐谱为一条 音序 。一个音序中至少应当定义 MIDI文件类型 、 音轨 数、 节拍 (和节拍定义方式)等基本信息,这些信息被记录在 Header Chunk 中,即便该音序中不包含任何音符信息——那你要这条音序干嘛。。。Header Chunk
/**
头部声明区 | 数据长度声明区 | 数据位
ASCII M ASCII T ASCII h ASCII d | Data Length: 6 | MIDI type: 1 | Track Count: 1 | Division: mode0, 120 ticsks/second
01010100 01101000 01100100 00000000 00000000 00000000 00000110 00000000 00000001 00000000 00000001 00000000 11110000
*/
首部块内容如上所示,用于声明一条音序的基本信息,仅用于标注文件声明。其中,4字节ASCII数据“MThd”为Header Chunk标识位,data length值为当前Chunk中数据位的长度。数据位包括3部分内容:
文件类型
2字节数据,整数。合法的MIDI文件应当属于以下三种类型的一种:- 0类MIDI:仅能容纳单一音轨;
- 1类MIDI:容纳多个音轨,所有音轨中的事件将按照既定时间戳同时播放——这也是应用最广泛的音序类型;
- 2类MIDI:容纳多个音轨,每个音轨中的事件将在上一条音轨结束之后开始播放。
音轨数
Header Chunk本身并不是音轨,其中以2字节记录除Header Chunk以外的其他类型Chunk(亦即 音轨 )数量。节拍定义/division
/**
|H|8-14bit 0-7bit
000 00000000
*/
如上结构的2字节数据位,这是个麻烦的字段。节拍有两种表示方式,取决于字段最高位(H位置处)数据:
- 0类节拍定义:0-14bit数据为一个整数,表示一个1/4音符(测试结论貌似持续0.5秒?)中的时间戳数量,值以120居多,简单粗暴;
- 1类节拍定义:更精确的节拍定义方式。0-7bit表示每一 SMPTE() 帧中的tick数;8-14bit表示每秒钟内的 SMPTE 帧数,详情可参考MTC Quarter Frame中的相关定义。这种定义方式应用不多。
Track Chunk
音轨 定义区。这实际上是一个逻辑分区,区分头部和数据部分。音轨/Track
/**
头部声明区 | 数据长度声明区 | 数据区 | 结束标记
ASCII M ASCII T ASCII r ASCII k | Data Length: unknown | OOXX.... | Ending the track: FF 2F 00
01010100 01110010 01101011 | OOOOOOOO XXXXXXXX OOOOOOOO XXXXXXXX | OOOOOOOO XXXXXXXX | 11111111 00101111 00000000
*/
音轨是一系列数据的集合,每条音轨以声明ASCII“MTrk”开始。音轨是一个完全不受限制的数据定义区域,其中记录乐谱各种信息,包含但不仅限于:版权声明、文本信息、开启/关闭声音(
Note On/Note Off
)、改变乐器(
Program Change
)等等。MTrk类型Chunk必须有
结束标记
,并且结束标记也应当计算入数据区总长度。
一个音序中通常包含至少1条音轨定义,并且仅在第一条音轨中保存类似版权信息、乐谱通用定义。非0类音轨也会通常将这些信息独立为一条音轨以方便维护。
音轨本身作为数据集合,与硬件载体并不存在任何关系。但是在实现过程中,通常建议一条包含Channel信息的Track仅对应一个Channel——亦即所保存的除公共信息(例如版权声明)之外的数据均应尽量操作同一个Channel。由于Channel总数不超过16个,因此一个MIDI乐谱中的Track数量通常也不会超过17条;更甚由于一个Channel同时只能维护一种乐器,因此通常情况下,一首MIDI乐谱也不会设计超过16种乐器混响。
下面我们聊一聊Track中的数据,以便解释一下上文这段话。啊,这将是个沉重的话题。
MIDI事件/Event
/**
* 一则简单版权声明事件
*
时间戳 | MIDI消息体
Tick | Message Type | Length | Message Data
11111111 00000010 00000010 01000110 01000101
*/
简言之,存储于Track数据区的所有内容均为一则
事件
,其结构如前文所示,主要包含两部分内容,
时间戳
和
MIDI消息
。事件是Track内容的基本组成单元,拥有时间戳是其固有属性。一则正确定义的事件将按照节拍的与定义方式(Header Chunk中的division)在合适的时间被合成器触发,并产生相应的效果。
时间戳/Tick
事件数据之间并不存在严格的边界,因此识别一条事件的依据便是消息体中严格定义的数据消息体长度和下一个事件的时间戳。在我们之前所定义的Header Chunk(1型MIDI,120tick/s)中,tick始终表示“当前事件的发生时刻”与“上一事件发生时刻”之间的时间刻度差值Delta Time。在编程模拟方面,维护“一个事件基于其他事件的严格顺序”是一件相当蛋疼的事情,这意味着可能得使用链表来管理一个乃至十万数量级(一首5分钟高清音序中的事件数规模在万级)的数据结构。Java中将Delta Time存储为“从音序开始执行以来所经历的tick”作为哈希的键值来存储和管理这些事件,一定程度上改善了检索、修改等操作的执行效率。可以参考 JDK6中的Track类的 源码。
注意由于一首乐谱的时间刻度可能会在规范允许的范围内被定义的相当密集,因此tick的数值理论上可以达到无限大。这可怎么办,设想我们的骡子早上叫了一声晚上叫了一声,这时这两个事件发生的时间间隔恐怕8字节的256个tick无法表达我们对于骡子的热爱之情。
因此,时间戳必须是一个 可变长度数 。
可变长度数
可变长度数允许不通过单独的length标记位记录一个长度不固定的正整数。对于可以通过7位以内二进制数表示的数据,看起来跟普通的二进制数没啥区别:/**
:
*/
对于超过127的数据,可变长度数需要扩展高位字节,并遵循如下规则:
- 所有可变长度数中,每个字节最高位用作标记位;
- 除非是最末位8个字节,否则当前字节中的标记位记1;
- 最末位8个字节标记位记0,该可变长度数结束。
/**
:
- 普通二进制:
- 可变长度数:
01111111
- 普通二进制:
11111111
- 可变长度数:
01111111
- 普通二进制:
00000000
- 可变长度数:
10000000 00000000
*/
MIDI消息
作为MIDI Event的组成部分,MIDI消息才是这通篇的主角,其在MIDI系统中的地位就是北方的面南方的米沙漠里面边下的雨。如MIDI事件一节所示,一个MIDI消息包含消息类型、数据长度、数据体三部分。一则乐谱中所有控制信息均存储于消息中,不同的资料会罗列不同的分类方式。通常按照存储消息解析方式的不同分其为四类: Meta消息 、 Sysex消息 、 控制消息 和 通道消息 。
Meta消息
虽然所有事件都应当被合成器识别,但Meta消息始终不会被响应为输出。Meta消息仅用于记录,如果一个音序是一页代码,Meta消息就是代码中的注释。所有Meta消息的消息类型均以"FF"开头作为标志:
/**
* 一则简单版权声明事件内的消息体
*
Meta消息
消息类型: FF 02 | Length:2 | ASCII: F ASCII: E
00000010 00000010 01000110 01000101
*/
上图所示的消息类型为“FF 02”,标准中定义其为“版权声明”。比较重要的Meta消息类型有“版权声明”(FF 02)和Track结束标记(FF 2F 00),在任何音序中都会存在。
Meta消息由于可被用于记录不定长数据,因此其中的Length位也为 可变长度数 ;数据位可以为特定格式标记、ASCII字符或为空(结束标记长度0、数据位空)。例如你可以声明一则长度为4字节的文本消息,然后像个不太能喝的司机被人猛灌一顿之后开着车在32车道的公路上随意发挥,甚至把莎士比亚全集录进去也是允许的,这都取决于Meta消息类型。
更多Meta消息类型可参考 附录二:Meta消息类型表 。
专用系统消息“F0/F7”/Sysex Message(注:此处存疑)
/**
* 系统专用信息结构
*
Meta消息
消息类型 | 0iiiiiii | 任意数据 | 结束标记
00000100 10010000 01111111 01111111 11110111
*/
一个系统专用信息如果以“F0”开始,则必须以“F7”作为结尾。因此,“F7”也被称作“End of Exclusive (EOX)”。
在多份同样的标准文档中,我看到多个关于此消息的不同解释,分别来自“ Standard MIDI-File Format Spec. 1.1 ”和“ Standard MIDI-File Format Spec. 1.1, updated ”。也希对此有深刻认识的同僚能够指出此文谬误。下文中给出两个不同的解释细则仅供参考,其中所列举的字节码亦并不保证正确性。
释义一:特定厂商专用扩展信息
专用系统消息(Syntactic System Exclusive Message)的名字来由于其只会对特定的合成器系统起作用。该消息允许对某一事件进行“加密”,合成器在读到该事件时检测“密文”是否与自己所定义的“密文”相匹配。如果不匹配,则忽略该整个事件;否则对其进行解析。
Sysex消息可以被用作一个“加密包裹”用于包裹任何其他类型的所有事件,也可用作 保留字段 用于包裹合成器自身的自定义事件(例如当前合成器实现的标准范围外的 控制器 类型),当然更多是用作发送针对合成器的特殊指令。MIDI中定义了许多 保留字段 用于后续扩展或交由合成器厂商自己实现,使用Sysex事件修饰的 保留字段 可以避免该消息被其他合成器错误解析,该扩展方式被称作合成器的“Universal Exclusive Messages”。
在此类释义下,该信息结构中的 0iiiiiii 字段即提供给合成器用于鉴别的“密文”: MIDI设备生产商ID ,固定长度为8bit并且最高位为0。合成器在不能识别该密文时应当将其中的直至结束标记位置的数据字段全部忽略;否则,解析内部数据。
在上例中,这则Sysex消息包裹了一条 Note On 信息。其余任何数据也将被允许。
释义二:系统高级信息
与释义一一样,该信息同样用于发送任意数据。在此类释义下,该信息结构中的 0iiiiiii 字段为包含结束标记在内的数据体长度,与所有其他消息的长度一样也为可变长度数。
虽然“F0”必须以“F7”作为结尾(此时作为标记位的F7并不会被识别为一条Event而被解析),但“F7”本身却可以独立使用,用于一条标准MIDI消息的承载。此时的“F7”被称作一个“escape”——实在不知道这术语翻译成中文应该叫啥,逃跑?跑的“F0”它娘都不认得?
这种设计的优势在于,可以允许分片发送大量数据。例如当多个“43 12 00”(据本人肤浅的视野目测,“43 12 00”不代表任何意义。。。)需要被发送时,可以使用如下的命令:
/**
* F0-F7承载命令示例(16进制)
*
// start from tick 0
F0 03 43 12 00 // start from F0
// 200-tick delta time
F7 06 43 12 00 43 12 00 // F7 as an escape
// 100-tick delta time
F7 04 43 12 00 F7 // F7 as an escape
// together with the end of F0 while EOX is count into the last escape.
*/
并且“F7”本身也可以作为独立消息使用:
/**
* F7作为escape使用
// start from tick 0
F7 01 FC // short message FC - pause
// 48-tick dalta time
F7 01 FB // FB - start
*/
关于该类型消息的更多解释,还可
点击此处参考更详细的字段拆解
。
控制消息
老实说苦于并未真正接触过硬件MIDI设备并且所见MIDI文件中大都不包含此类信息,鄙人对于此类消息的理解亦完全基于机翻的文字描述,难免会有所纰漏,同时也希望能有此领域专业人士批评指正。非通道相关的控制类消息的部分工作已经被现今在诸多成熟的合成器默认实现了,例如“音调调整”被设计用于兼容老式可能存在音调不准的合成器,现在已经基本废弃。一些特殊的控制类消息可能并不会存在于MIDI文件中,而是被设计于输入设备上发往合成器(例如某个按钮)用于实时的播放控制。
系统专用消息是一类特殊的控制信息。
控制消息包括两部分,“系统通用消息”和“实时系统消息”。控制类消息均对合成器起作用,其效果可能表现于整个音序的播放阶段。在上文中的“FC”、“FB”即为两个控制消息,分别代表“暂停”和“开始”。更多关于控制消息的细节请移步 附录三:控制消息类型表 。
通道消息/Short Message
/**
* 通道消息标准格式
*
类型和通道 | 数据1 数据2
ttt cccc 0xxxxxxx 0yyyyyyy
0000 01111111 01111111
*/
好吧,怀旧一下。Short Message其实是Java中的叫法,Java把控制消息和通道消息全部合并称为短消息。
顾名思义,这种类型消息与通道相关,这也是到目前为止唯一能跟通道和音轨之间的联系扯上点关系的数据了。一则通道消息包含2个或3个字节,并且没有长度标记。对于合成器而言,识别通道消息的标识就是高位字节首位为“1”并且紧邻1个或2个首位为“0”的低位字节(其他类型消息通过数据区长度或结束标记指定)。至于为何没有首位为0(状态位数值在128以下)的MIDI消息,鄙人目测也是为了防止消息数据区被数识别为另一则消息造成数据污染。
如上图所示,通道信息中的“ttt”为信息类型标识,“cccc”为“0~15”共计16个通道标识,这也是为何MIDI通道只有16个的原因——超出了就根本表示不出来么...“数据1”和“数据2”所指代的含义有异于不同的通道信息类型。
根据“ttt”的不同,通道类消息包括 Note Off 、 Note On 、 (触后)键力度调整/Polyphonic Key Pressure(After Touch) 、 控制器变化/Control Change 、 程序变化(切换乐器)/Program Change 、 (触后)通道压力调整/Channel Pressure(After Touch) 和 音轮调节/Pitch Wheel Change 共计7个。虽然数据格式通常是一定的,但是不同的合成器对于各类消息的几个字段解释可能有异。以下涉及实例部分均以使用最广泛的 GS波表软件合成器 为例讲解。
全部通道消息细节信息烦请查阅“ 附录四:通道消息类型表 ”。
- Note On(1001)/Note Off(1000)
On/Off信息往往成对出现,开启和关闭音符,表现为发声和停止发声,在硬件设备上(例如钢琴)就是按下和松开一个按键。此时数据1表示发声音调,数据2表示按键力度(表现为音量,当然了敲键盘越重钢琴越响)。这类似于在一个有16条通道的排水管中,打开和关闭闸门,当“闸门”打开时音乐数据流便被允许通过该排水管,如果“闸门”没被关闭则此类型的水流(当前音调和力度)将会一直存在于该通道。指定了相同音调的“Note Off”信息将关闭信息流,“Note Off”中的力度信息将表示音符的松开力度。“附录六”中给出了完整的128个 Note Key 与音阶的对应关系。
点此下载《24tick/秒的 Piano MIDI 样例》。/** * 以最大力度在 Channel 0 按下C大调,并在持续240个tick单位后松开 * C大调为从0到127号中的第60号,0x3C;中调64号为第四音阶E调,0x40。 * GS波表软件合成器中每1/4个音符持续0.5秒,故该Note共持续5秒。 * 10010000 00111100 01111111 01110000 10000000 00111100 01111111 */
- 程序变化/Program Change(1100)
由于较常用所以插个队~~Meta消息中的“FF 04”消息用于标注“乐器名称”。然而实际上,其仅用于标注,并不会对通道上的音频流产生实质影响。真正能产生影响的就是该 Program Change 消息,其中的数据1为128为音色号,数据2为空。在默认(未设定 Program Change 事件)情况下,除了第10号通道以外,其余通道均使用与当前通道号一致的音色;每个通道(包括10号通道)均会响应程序切换事件并对该事件之后的所有Note采用新设定的音色(Program Change 之前的事件不受设定音色影响)。
点此下载《24tick/秒的 Percussive MIDI 样例》。/** * 切换 Channel 0 当前默认乐器“0-Piano”为“17-Percussive”; * 以最大力度在 Channel 0 按下C大调,并在持续240个tick单位后松开; * 11000000 00010001 10010000 00111100 01111111 01110000 10000000 00111100 01111111 */
- 触后键力度/Polyphonic Key Pressure(1010)
弹钢琴时一屁股坐上琴键并一直坐在上面然后站起来,跟用手按下之后缓慢回弹——这两种情况发出的声音肯定是不一样的。触后键力度即用于表示上一个琴键被按下(Note On)之后,在松开之前该琴键的保持力度。
由于是触后量,因此在此事件之前已经被关闭的音符将不会受到力度影响。注意,该事件将影响当前通道上所有处于播放状态或即将被打开的音符。
需要注意的是,部分音色(经典例如钢琴)在一个时间足够长的 Note On 和 Note Off 之间,也即在该音调完整播放时本身就有音量衰减。甚至对于钢琴而言,标准音色在 Note Off 时声音并不会立即消失,而是以更快的速度将力度渐变至0。这是为了还原此类音色在真实乐器上的演奏效果,当然也存在多数音色将始终在 Note On 和 Note Off 之间保持同一播放效果。大部分合成器均能正确根据音色库的定义表现这些音色的特效,然而对于预先录制为.mp3格式的音色库而言,模拟的合成器对在 Note Off 上的处理将会变得相当尖锐和突兀。看看名噪一时的Color Piano,试着弹几个键就知道我在说什么了。这时合成器应当能够主动为特定音色添加触后量变化消息,以实现渐变的延音效果。/** * 以最大力度在 Channel 0 按下C大调,并在240个tick单位内让声音渐变至消失 * 每个事件之间的tick间隔为1, * 约每2个tick单位压力降低1 * 11000000 00010001 // Program Change 10010000 00111100 01111111 // Note On 10100000 00111100 01111111 // PolyKey Pressure 10100000 00111100 01111110 10100000 00111100 01111110 ... // 剩余237个触后键力度调整 10100000 00111100 00000000 10000000 00111100 00111111 // Note Off */
在以上两则Midi示例中,《Piano MIDI 样例》属于会渐弱的音色,而《Percussive MIDI 样例》不存在该效果,其音色信息将会一只持续到对应的 Note Off 被解析或(如果没有被关闭)直至音轨末尾。
注:由于成本原因,大多数MIDI键盘制造商并不单独为总数128的每个键提供触后压力变化输入。因此,该信息也并不能在所有合成器上被正确识别。要达到类似的效果,请使用第11号控制器。点此下载24tick/秒的 Percussive MIDI 声音渐弱样例,使用触后键力度变化;推荐尝试可被Windows默认合成器正确识别的11号-力度控制器版本。
- 控制器变化/Control Change(1011)
控制器是MIDI创作的精髓所在。特殊音效例如颤音、混响声度、声相偏移等等操作都需要通过控制器完成,控制器的完整程度和功能强弱也一直是极客玩家们选择数字音乐产品的重要指标。可惜屌丝从来没玩过那么高端的玩意。。。对于附录五:控制器类型表中所罗列的控制器也就到了能认识那些字的程度。。。也欢迎能有高端玩家留下关于控制器的模拟实现细节的完善意见。控制器调整对当前通道中位于其后的所有音符起效。
在总128个控制器中,
点此下载《24tick/秒的 Percussive 颤音渐弱样例》/** * 以最大力度在 Channel 0 按下C大调,并在240个tick单位内让声音渐变至消失 * 使用11号-力度控制器替代触后键力度实现渐变 * 在第2.5秒单位时为音符添加最大力度颤音 * 11000000 00010001 // Program Change 10010000 00111100 01111111 // Note On 10110000 00001011 01111111 // 0xB0 0x11 0x7F - 11号控制器 10110000 00001011 01111111 10110000 00001011 01111110 ... // 中间117个11号控制器 10110000 00000001 01111111 // 0xB0 0x01 0x7F - 1号控制器,120tick时最大力度颤音 ... // 中间120个11号控制器 10110000 00001011 00000000 // 0xB0 0x11 0x00 - 11号控制器最小力度 10000000 00111100 00111111 // Note Off */
- 通道压力调整/Channel Pressure(1101)
与 Program Change 类似,该消息仅包含一个数据位。其作用与“触后按键力度调整消息”类似,但该事件被设计用于某些不能正确响应和弦(触后)键力度的合成器,将对当前所指定的音轨中所有正在播放的Note里拥有最高力度的Note执行压力变化(一说将对通道内所有音符执行压力变化——鬼特么知道,GS不支持,所以还是继续用11号全通道吧...)。为了避免混响时出现意想不到的情况,通常建议使用单通道的Mono模式(参考附件四中关于通道模式信息中的描述)。
- 音调轮变化/Pitch Wheel Change(1110)
“触后键压力调整”用于调整按键的保持力度,与之对应,音调轮则用于处理音符播放时的音调偏移,不过该偏移对当前通道中所有正在播放的音符生效,数据1和数据2共组成14位二进制数表示音调偏移量,比较诡异的是此处数据1为低位字段,即可用于微调;数据2为高位字段。该可用于产生音调的渐变,实现圆润的滑音效果。在GS波表软件合成器中,只能实现粗调——消息数据1无意义,数据2表示精度为127的调音轮变化,其中值为0x40,十进制64,也即二进制01000000。与触后键力度一致,该变化也属于触后量。
点此下载《24tick/秒的 Mono 模式 Channel 0 下 Percussive 颤音渐弱音调渐变样例》,或者看看残忍之快速的巨神之刃放松放松。/** * 以最大力度在 Channel 0 按下C大调,并在240个tick单位内让声音渐变至消失 * 在第2.5秒单位时为音符添加最大力度颤音 * 在 240 tick 内,仅使用粗调将音符提升一个音阶 * 约每3个tick单位音调提升1 * 11000000 00010001 // Program Change 10010000 00111100 01111111 // Note On 10110000 00001011 01111111 // 0xB0 0x11 0x7F - 11号控制器 11100000 00000000 01000000 // 0xE0 0x00 0x40 - 音调轮中值 10110000 00001011 01111111 11100000 00000000 01000000 10110000 00001011 01111110 11100000 00000000 01000001 // 0xE0 0x00 0x41 ... // 117个11号控制器和音调轮变化 10110000 00000001 01111111 // 0xB0 0x01 0x7F - 1号控制器,120tick时最大力度颤音 ... // 120个11号控制器和音调轮变化 10110000 00001011 00000000 // 0xB0 0x11 0x00 - 11号控制器最小力度 11100000 00000000 01111111 // 0xE0 0x00 0x7F - 音调最高偏移 10000000 00111100 00111111 // Note Off */
js生成数字音乐
啧啧。。。目前看起来,为了能把我们想要的骡子声用来挂门上辟邪,我们的至尊8合1小霸王可能得加点位宽才顶得住。不过好歹也算是有些眉目了,我们已经生成了一串串可以出声的字节,接下来只需要把这一坨东西写出为一个".mid"文件就行。因此,“Js生成数字音乐”步骤中一个亟待解决的问题是: 用Javascript抽象Midi文件结构 。
浏览一下关于MIDI文件格式的定义,在“音调轮变化”一节实例《残忍之快速的巨神之刃》中的二进制串已经包含了一首完整Midi所需要的大部分数据,让我们补充一些组成一首完整Midi文件所需的细节内容:一个 Header Chunk,Track Chunk 头和结束标记(0xFF 0x2F 0x00),上文中提到的通道模式调整和版权声明。差不多够了,所以最后这坨东西看起来可能像下面这样——这将是一个拥有2001字节的数据串。
/**
* 完整版“残忍之快速的巨神之刃”
*
01010100 01101000 01100100 // MThd
00000000 00000000 00000110 // header lentgh: 6 bytes
00000001 // Midi Type: 1
00000001 // Track count: 1
00011000 // Division: 48 ticks / second
01010100 01110010 01101011 // MTrk
00000000 00000111 10111011 // Track Length: 955 bytes - length-fixable data
11111111 00000010 00011100 // tick 0, 0xFF 0x02 0x1C - CopyRight, length: 28 bytes
01100011 00101001 00110010 // CopyRight Contents in 28 bytes.
00110001 00110011 00100000
01111001 00100000 01111001
01110010 01101101 01111001
01110010 01100011 01101000
01101100 01101001 01110110
00101110 01100011 01101110
11000000 00010001 // Program Change
10010000 00111100 01111111 // Note On
10110000 00001011 01111111 // 0xB0 0x11 0x7F
11100000 00000000 01000000 // 0xE0 0x00 0x40
10110000 00001011 01111111
11100000 00000000 01000000
10110000 00001011 01111110
11100000 00000000 01000001 // 0xE0 0x00 0x41
// 117 No.11 Controller
10110000 00000001 01111111 // 0xB0 0x01 0x7F
// 120 No.11 Controller
10110000 00001011 00000000 // 0xB0 0x11 0x00
11100000 00000000 01111111 // 0xE0 0x00 0x7F
10000000 00111100 00111111 // Note Off
11111111 00101111 00000000 // tick 0, 0xFF 0x2F 0x00 - End of Track Chunk
*/
有兴趣的同僚们可以用二进制文件编辑器,例如UltraEditor之类的打开《残忍之轻快的巨神之刃》,对照以上二进制数据。如果发现有些不一样的地方——不要在意那些细节啦——这文件打开之后看起来大概像这样:

Blob写出文件/Midi数据序列化
Html5的 File API 绝逼是个臣诚惶诚恐死罪死罪的伟大发明,尤其是Blob这样的变态玩意——允许将任意数据序列化出为可被传输的二进制数据。啊,骡子出头有望,我们可以把上面一系列数据改造成一个Unit8数组,再通过Blob写出即可实现挂墙上的宏伟目标。var dataString = "", // contents of the midi given above
dataArray = dataString.split(/\D/).map(function(str) {
return parseInt(str, 2);
}),
blob = new Blob([new Uint8Array(dataArray)], {type:"audio/midi"});
有了Blob事情就好办多了,该文件在内存中创建完毕,已经可以通过DataUrl提供访问了。设想我们让用户点击直接下载:
var url = URL.createObjectURL(blob);
document.body.onclick = function() {
window.open(url);
document.body.onclick = "";
}
类组织结构
当然我们不能总是用二进制说话,骡子肯定是听不懂的。我们倒是可以把Midi文件结构中的数据部分按类别划分出来,并创建一系列便于访问并且兼顾性能的接口用于操纵文件数据。UML图就不画了,直接贴5年前的——吐槽一下,这领域的更新足见有多让人捉襟见肘,数年前的资料放在如今仍然适用,没准还会一直适用下去。。。
通过基于tick哈希的设计对上述UML图进行优化,我们可以很容易整理出如下的一些基本类,其核心为三个数据层: Message 、 Track 和 Sequence 。
Message
Midi音序的基本数据单元,维护 type 和 data 两个字段并提供相应的增改功能,同时提供序列化支持。需要注意的是对于除通道类消息之外的消息类型,序列化时需要在type和data之间添加可变长度数表示的数据字段长度。Track
基于tick哈希设计的Message容器。此处由于需要对tick进行直接管理,因此在Message类和Track类中间省略MidiEvent层。作为容器对象,提供基本的容器操作,并提供序列化的支持。通过以上简单探讨,Track作为容器应当具备以下一些特性:
- 事件允许重复。即在相同时间戳里允许产生在字节范围内完全一致的多个消息;
- 事件严格有序。同一个时间戳内,Program Change 和 Note On 位置的调换会完全改变输出音符的音色,因此事件之间必须有序;
- 提供尽量高效的访问。单一Track可维护的事件最多为四个字节表示的可变长度数:01111111 01111111 01111111 11111111——即268435455个,这是个相当可观的数据。设想在一个节奏一定的音序中,混响音符的节拍之间的间隔往往是固定的,这也意味着表示这些音符的Midi事件之间 Delta Tick 将会是一些固定的整数。这也是以初始偏移量作为 tick 并哈希的主要原因所在。
- 序列化时进行时间戳转换。将从音序初始的时间戳数值转换为当前事件与上一事件之间的时间戳差值。
- 为了提供尽量还原Midi标准的自由度,Track将不对Channel和Instrument做任何限制,亦即允许该类的使用者只有设定当前Track与Channel和Instrument的映射关系,而无需遵循之前所描述(为了便于维护和管理而采用Mono模式、单一Track使用一种音色)的种种约束条件。
/**
* key: tick count from the start of the sequence.
* value: msgArr {Array<Message>} messages that're sharing the same tick which should
* be contributed by type and data, and they could be serialized.
*/
events = {
: [
x903F7F,
// other messages in tick 0
],
: [
x803F7F,
// other messages in tick 120
]
}
Sequence
我们直接跳过了作为逻辑结构的“Chunk”。同时 Header Chunk 作为一个Midi乐谱的必备数据,因此可以直接拆解为音序的一系列属性,甚至可以写入构造函数。从这个角度看,Sequence是一个纯粹的Track容器,提供基本容器方法和对 Header Chunk 属性的访问,并允许序列化。扩展和封装
我一直怀疑自己是不是点错了技能树。讨论了大半篇幅到太阳都快落山了的Midi文件数据,三个基本类就已全部囊括。如果我们再提供一些对于动态加载、二进制/十六进制数据运算、用于传递数据用的Base64编码和Blob支持,就几乎得到了一个比较完善的 数字音乐模拟系统 ——的数据部分,亦即上文中UML图的Sequencer以下的部分。当然我在 这里 已经做了一部分工作,也欢迎大家前去围观批评。对于我们在今天早些时候提到的“《残忍之轻快的巨神之刃》”,代码就可以这样写了:
var seq = new Midi.Sequence(1, 24),
track = seq.getTrack(0);
track.addEvent(0, new Midi.Message(0xc0, 0x11)); // Program Change to No.17
track.addEvent(0, new Midi.Message(0xB0, [124,0])); // omni on
track.addEvent(0, new Midi.Message(0xB0, [126,1])); // mono on & omni on
track.addEvent(0, new Midi.Message(0x90, [60,127])); // Note On Key 0x3C
track.addEvent(120, new Midi.Message(0xB0, [1,127])); // Vibrato Controller
track.addEvent(240, new Midi.Message(0x80, [60,127])); // Note Off
// Apply Voice Change and Pitch Wheel Change
for (var i = 1; i <= 240; ++i) {
track.addEvent(i, new Midi.Message(0xB0, [11, 127 - ~~((127 / 240) * i)]));
track.addEvent(i, new Midi.Message(0xE0, [0, 64 + ~~((63 / 240) * i)]));
}
// Click & Download
var blob = new Blob([seq.toByteArray(1)], {type:"audio/midi"});
var url = URL.createObjectURL(blob);
document.body.onclick = function() {
window.open(url);
document.body.onclick = "";
}
你也可以
到这里来现场玩一玩
,或者
点击这里查看源码
。
MIDI实时播放
当然把一个Midi乐谱下载到本地用GS波表软件合成器播放肯定是不存在问题的。但是对于一个存在实时交互的系统,合成器应当能够对用户输入做出反应,这是“下载文件并交由本地合成器播放”策略所不能解决的问题。设想我们希望在浏览器端创建最初所提到的完整数字音乐模拟系统,允许用户自由创作音符并实时对以创作的内容进行修改,该模拟应当可以对用户的输入进行反馈,即提供按键或操作的声音预览。前文中我们致力于并成功解决了 用Javascript解析和封装Midi乐谱 的问题,下一个问题是: 产生的Midi序列如何实时播放?
浏览器不能直接解析Midi文件,因为浏览器中并不集成音色库,也没有合成器。访问声卡合成器的功能就别想了,几年内是出不来的,看看使用显卡的硬件加速进程就知道。不过仍然有多种方法可供选择。
- MP插件:一种方法是,把GS波表软件合成器搬到浏览器中来。通过给浏览器安装 Media Player 插件即可实现,输入文件Url播放;
- Flash插件:Flash中已经有多个版本成熟的合成器可以使用,例如大名鼎鼎的AudioTool。鉴于鄙人浅薄的乐理知识,这么高端的东西还是不去碰了。。。
- Java插件:Java真是一门伟大的语言,JDK从7.0开始提供了从Midi文件系统到合成器的全部实现。
- 预处理的音色库:前文中提到过的MIDI.js就采用了这种方法,他们将完整的音色库定义转换成Ogg文件并使用base64编码,以便浏览器可以直接播放。Udcobe团队创造了一种非常靠谱的声音转换算法,用于将Midi乐谱通过音色库渲染成Ogg,再转换为压缩音频。旧式的录音棚使用高保真录音来实现从Midi到压缩音频的转换,相比之下Udcube的算法更具优越性,免去了对高端声卡的需求和不必要的干扰。
也就是说,我们需要在浏览器端使用 Html5 Audio API 编码一个浏览器端的 合成器 。
浏览器端的合成器
这是另一个话题,该话题的含量甚至已经超过了这篇文章到目前为止加上附录的所有内容长度,现在也还没有这方面案例可以参考。这也是鄙人今后相当长一段时间内的调研方向,FML...基于以上原因此处不对合成器的模拟实现做过多讨论,因为......我TM也不懂啊!玩蛋去。。。下面是一些可用的参考,制作列举不做评论。
- Java 7.0 中关于sf2文件的解析源码;
- 日本哥Toyoshim的开源项目 T'Sound System Library 中音色库文件的js解析源码。
现状&将来
总的来说 Web MIDI 的发展势头还是很强劲的,无奈专业设备的准入门槛实在不在鄙人这样的资深屌丝能承受之范围,各种高大上合成器应用也非我这五音不识的认知所能理解。不过可惜的是, HTML5 MIDI API 对合成器和模拟方面似乎并没有什么兴趣,该草案2013年的主要进展仍然是尝试在浏览器中添加MIDI硬件设备接口——这也基本上宣布了该草案在成型之内一两年里的缓步发展状况,因为目前还未有纯粹基于浏览器的合成器面世,键盘接进浏览器也暂时还处于只能玩蛋的状态——兴许后续的扩展中会有浏览器厂商愿意解决合成器的问题,即便这意味着得招一帮既懂音乐又会写内核的人来开发这一系列API的实现——不过谁知道呢,在那之前我们可能还得继续关注并着手解决如何才能够实现让挂在浏览器里的骡子音色可以用来驱灾撵鬼的问题。最后附上 大树阴凉儿 的成名曲 猴子.mid 。这首曲子虽然看起来做得很粗糙,但却有其之所以值得铭记的独到原因——它是数年前由好基友一个字节一个字节敲出来的。
谢谢观赏,后会有期。
http://ju.outofmemory.cn/entry/79263
http://yarmyarch.com/archives/141
附录
附录一:GS波表软件合成器音色表(GM2标准)
钢琴 | ||
0 | Acoustic Grand Piano | 大钢琴(声学钢琴) |
1 | Bright Acoustic Piano | 明亮的钢琴 |
2 | Electric Grand Piano | 电钢琴 |
3 | Honky-tonk Piano | 酒吧钢琴 |
4 | Rhodes Piano | 柔和的电钢琴 |
5 | Chorused Piano | 加合唱效果的电钢琴 |
6 | Harpsichord | 羽管键琴(拨弦古钢琴) |
7 | Clavichord | 科拉维科特琴(击弦古钢琴) |
色彩打击乐 | ||
8 | Celesta | 钢片琴 |
9 | Glockenspiel | 钟琴 |
10 | Music box | 八音盒 |
11 | Percussive | 颤音琴 |
12 | Marimba | 马林巴 |
13 | Xylophone | 木琴 |
14 | Tubular Bells | 管钟 |
15 | Dulcimer | 大扬琴 |
风琴 | ||
16 | Hammond Organ | 击杆风琴 |
17 | Percussive Organ | 打击式风琴 |
18 | Rock Organ | 摇滚风琴 |
19 | Church Organ | 教堂风琴 |
20 | Reed Organ | 簧管风琴 |
21 | Accordian | 手风琴 |
22 | Harmonica | 口琴 |
23 | Tango Accordian | 探戈手风琴 |
吉他 | ||
24 | Acoustic Guitar (nylon) | 尼龙弦吉他 |
25 | Acoustic Guitar (steel) | 钢弦吉他 |
26 | Electric Guitar (jazz) | 爵士电吉他 |
27 | Electric Guitar (clean) | 清音电吉他 |
28 | Electric Guitar (muted) | 闷音电吉他 |
29 | Overdriven Guitar | 加驱动效果的电吉他 |
30 | Distortion Guitar | 加失真效果的电吉他 |
31 | Guitar Harmonics | 吉他和音 |
贝司 | ||
32 | Acoustic Bass | 大贝司(声学贝司) |
33 | Electric Bass(finger) | 电贝司(指弹) |
34 | Electric Bass (pick) | 电贝司(拨片) |
35 | Fretless Bass | 无品贝司 |
36 | Slap Bass 1 | 掌击Bass 1 |
37 | Slap Bass 2 | 掌击Bass 2 |
38 | Synth Bass 1 | 电子合成Bass 1 |
39 | Synth Bass 2 | 电子合成Bass 2 |
弦乐 | ||
40 | Violin | 小提琴 |
41 | Viola | 中提琴 |
42 | Cello | 大提琴 |
43 | Contrabass | 低音大提琴 |
44 | Tremolo Strings | 弦乐群颤音音色 |
45 | Pizzicato Strings | 弦乐群拨弦音色 |
46 | Orchestral Harp | 竖琴 |
47 | Timpani | 定音鼓 |
合奏/合唱 | ||
48 | String Ensemble 1 | 弦乐合奏音色1 |
49 | String Ensemble 2 | 弦乐合奏音色2 |
50 | Synth Strings 1 | 合成弦乐合奏音色1 |
51 | Synth Strings 2 | 合成弦乐合奏音色2 |
52 | Choir Aahs | 人声合唱“啊” |
53 | Voice Oohs | 人声“嘟” |
54 | Synth Voice | 合成人声 |
55 | Orchestra Hit | 管弦乐敲击齐奏 |
铜管 | ||
56 | Trumpet | 小号 |
57 | Trombone | 长号 |
58 | Tuba | 大号 |
59 | Muted Trumpet | 加弱音器小号 |
60 | French Horn | 法国号(圆号) |
61 | Brass Section | 铜管组(铜管乐器合奏音色) |
62 | Synth Brass 1 | 合成铜管音色1 |
63 | Synth Brass 2 | 合成铜管音色2 |
簧管 | ||
64 | Soprano Sax | 高音萨克斯风 |
65 | Alto Sax | 次中音萨克斯风 |
66 | Tenor Sax | 中音萨克斯风 |
67 | Baritone Sax | 低音萨克斯风 |
68 | Oboe | 双簧管 |
69 | English Horn | 英国管 |
70 | Bassoon | 巴松(大管) |
71 | Clarinet | 单簧管(黑管) |
笛 |
|
|
72 | Piccolo | 短笛 |
73 | Flute | 长笛 |
74 | Recorder | 竖笛 |
75 | Pan Flute | 排箫 |
76 | Bottle Blow | [中文名称暂缺] |
77 | Shakuhachi | 日本尺八 |
78 | Whistle | 口哨声 |
79 | Ocarina | 奥卡雷那 |
合成主音 | ||
80 | Lead 1 (square) | 合成主音1(方波) |
81 | Lead 2 (sawtooth) | 合成主音2(锯齿波) |
82 | Lead 3 (caliope lead) | 合成主音3 |
83 | Lead 4 (chiff lead) | 合成主音4 |
84 | Lead 5 (charang) | 合成主音5 |
85 | Lead 6 (voice) | 合成主音6(人声) |
86 | Lead 7 (fifths) | 合成主音7(平行五度) |
87 | Lead 8 (bass+lead) | 合成主音8(贝司加主音) |
合成音色 | ||
88 | Pad 1 (new age) | 合成音色1(新世纪) |
89 | Pad 2 (warm) | 合成音色2 (温暖) |
90 | Pad 3 (polysynth) | 合成音色3 |
91 | Pad 4 (choir) | 合成音色4 (合唱) |
92 | Pad 5 (bowed) | 合成音色5 |
93 | Pad 6 (metallic) | 合成音色6 (金属声) |
94 | Pad 7 (halo) | 合成音色7 (光环) |
95 | Pad 8 (sweep) | 合成音色8 |
合成效果 | ||
96 | FX 1 (rain) | 合成效果 1 雨声 |
97 | FX 2 (soundtrack) | 合成效果 2 音轨 |
98 | FX 3 (crystal) | 合成效果 3 水晶 |
99 | FX 4 (atmosphere) | 合成效果 4 大气 |
100 | FX 5 (brightness) | 合成效果 5 明亮 |
101 | FX 6 (goblins) | 合成效果 6 鬼怪 |
102 | FX 7 (echoes) | 合成效果 7 回声 |
103 | FX 8 (sci-fi) | 合成效果 8 科幻 |
民间乐器 | ||
104 | Sitar | 西塔尔(印度) |
105 | Banjo | 班卓琴(美洲) |
106 | Shamisen | 三昧线(日本) |
107 | Koto | 十三弦筝(日本) |
108 | Kalimba | 卡林巴 |
109 | Bagpipe | 风笛 |
110 | Fiddle | 民族提琴 |
111 | Shanai | 山奈 |
打击乐器 | ||
112 | Tinkle Bell | 叮当铃 |
113 | Agogo | [中文名称暂缺] |
114 | Steel Drums | 钢鼓 |
115 | Woodblock | 木鱼 |
116 | Taiko Drum | 太鼓 |
117 | Melodic Tom | 通通鼓 |
118 | Synth Drum | 合成鼓 |
119 | Reverse Cymbal | 铜钹 |
声音效果 | ||
120 | Guitar Fret Noise | 吉他换把杂音 |
121 | Breath Noise | 呼吸声 |
122 | Seashore | 海浪声 |
123 | Bird Tweet | 鸟鸣 |
124 | Telephone Ring | 电话铃 |
125 | Helicopter | 直升机 |
126 | Applause | 鼓掌声 |
127 | Gunshot | 枪声 |
附录二:Meta消息类型表
Meta Message | ||
FF 00 02 ss ss | ||
这是一个可选的事件,它只能产生在第一个track,并且在非零时刻之前。 格式2文件中,这个用来识别每个 track,如果忽略,这个序列号从而用 track 出现的次序表示。. 格式1文件中,这个事件只能产生在第一个 track。 | ||
ss ss | 音序号,16 bit 二进制数。 | |
FF 01 <长度> <数据> | ||
这个事件是用来注释 track 的文本。 | ||
<长度> | <文本> 的长度(可变长度数) | |
<文本> | <长度>个字节的 ASCII 文本或8位二进制数 | |
FF 02 <长度> <数据> | 版权声明 | |
这个事件是用ASCII文本表示的版权通告, 若要使用该事件,其必须为第一个track的第一个事件。 | ||
FF 03<长度> <数据> | ||
音序或 track 的名称,同样由可变长度数与ASCII数据组成。 | ||
FF 04 <长度> <数据> | 乐器名称 | |
这个用来详细的记述(这个) MIDI channel 在 (这个)track 里使用的乐器。注意该事件并不会真正影响Channel中的音频流。如果希望改变当前Channel中的乐器(而非仅仅标注),请使用Program Change。 | ||
FF 05 <长度> <数据> | 歌词 | |
通常每个音节都有自己的歌词,MIDI中允许将ASCII歌词嵌入乐谱作为消息并加上时间戳,交由合成器解释。 | ||
FF 06 <长度> <数据> | ||
通常用于格式0或格式1的第一个 track 。 | ||
FF 07<长度> <数据> | 暗示 | |
用来表示舞台上发生的事情。如:“幕布升起”、“退出,台左”等。 | ||
FF 20 01 cc | MIDI Channel 前缀 | |
关联紧跟的 所有meta-events 或 sysex-events 的 MIDI channel,直到出现下一个包含Channel信息的标准消息出现。 | ||
cc | MIDI channel 1-16 | |
FF 2F 00 | Track 结束标记 | |
这个事件是必须的,并且时间戳刻度应当在所有其他事件之后。 | ||
FF 51 03 tt tt tt | ||
这个标注1/4音符的速度,用微秒表示。这个意味着改变一个 delta-time 的单位长度。 (注意1) 如果没有指出,缺省的速度为 120拍/分。这个相当于 tttttt = 500,000。 | ||
FF 54 05 hh mm ss fr ff | ||
可选事件,描述 track 开始时的 SMTPE 时间。 这个事件必须发生在非零 delta-time之前,且在第一个事件之前。 在格式1中,这个事件必须在第一个 track 中。 | ||
hh mm ss fr | 小时/分/秒/帧 用 SMTPE 格式。 | |
ff | Fractional frame, in hundreth's of a frame | |
FF 58 04 nn dd cc bb | ||
拍子记号的形式: nn/2^dd 这个参数 cc 是表示每个 MIDI 时钟的节拍器的 tick 数目。 通常24 个 MIDI 时钟为一个1/4音符。可是一些软件允许用户自己设置这个值。参数 bb 定义:24 MIDI 时钟(这个“一般”(表示)1/4音符)中 1/32音符的数目。 | ||
nn | 拍子记号,分子 | |
dd | 拍子记号,分母表示为 2 的(dd次)冥。 | |
cc | 每个 MIDI 时钟节拍器的 tick 数目。 | |
bb | 24个MIDI时钟中1/32音符的数目(8是标准的)。 | |
FF 59 02 sf mi | 音调符号 | |
音调符号,表示升调或降调值,大调或小调的标志。 0 表示 C 调,负数表示“降调”,正数表示“升调”。 | ||
sf | 升调或降调值 | |
mi | 0 = 大调 | |
FF 7F <len> <id> <data> | 音序器描述 Meta-event | |
这个在 MIDI 文件中等同于系统高级事件。 在 MIDI 文件中用这个事件表示制造商音序器统一化的描述。 | ||
<长度> | 长度 of <id>+<数据> (可变长度数) | |
<id> | 1或3个字节表示制造厂商。 | |
<数据 > | 8位二进制数 |
附录三:控制消息类型表
系统通用消息(System Common Message) | ||
状态位(D7-D0) | 数据位(D7-D0) | 信息含义 |
11110000 | 0iiiiiii | 专用系统消息 |
0ddddddd | 见前文中关于“SysexMessage”的描述。 | |
11110001 | / | MIDI时间代码转换信息 |
11110010 | 0lllllll | 乐曲位置指针信息 |
0mmmmmmm | 这个信息是一个内部十四位寄存器存储了从乐曲开始计数时的MIDI的节拍数(在MIDI协议中,一节拍相当于六个MIDI时钟单位) | |
11110011 | 0sssssss | 乐曲选择信息 |
该信息指定了以什么序列或哪首乐曲将被演奏。 | ||
11110100 | / | 未定义 |
11110101 | / | 未定义 |
11110110 | / | 音调调整要求信息 |
当模拟合成器收到这个信息时,都要调整它们的震荡器的震荡频率,这个信息是为老式的电子合成器而保存的.因为在老式的合成器的使用时常常发生音调不准,需要此信息对其进行震荡器的微调.而今天的合成器已经不需要它了。 | ||
11110111 | / | 结束系统专用信息 |
系统实时消息(System Real-Time Message) | ||
状态位(D7-D0) | 数据位(D7-D0) | 信息含义 |
11111000 | / | 时钟信息 |
当有同步要求时,该信息每四分之一音符发送24次。 | ||
11111001 | / | 未定义 |
11111010 | / | 开始信息 |
开始现有的序列演奏(通常时钟信息紧跟在本信息后面连用)。 | ||
11111011 | / | 继续信息 |
本信息是命令音序在被停止的地方继续演奏。 | ||
11111100 | / | 停止信息 |
停止/暂停当前音序,可被恢复。 | ||
11111101 | / | 未定义 |
11111110 | / | 联系激活信息 |
这条信息的使用是可以选择的。如果使用了该信息,接收器将在每300(最大值)毫秒内准备接收下一个联系激活信息,否则它会自动认为连接已经终止。如果接收器认为连接已经终止后,它将停止所有发音并恢复到正常的工作状态(非联系激活工作状态)。 | ||
11111111 | / | 复位信息 |
复位信息将系统内所有接收器都恢复到电源打开的初始状态。在一些特殊状态下,它可以不复位到电源打开状态下。 |
附录四:通道消息类型表
通道声音信息(Channal Voice Message) | |||
状态位(D7-D0) | 数据位(D7-D0) | 信息含义 | |
1000cccc | Onnnnnnn | 音符关闭 | |
Ovvvvvvv | 这个信息是在一个音结束时发出的,如键盘的某个键被放开 | ||
1001cccc | Onnnnnnn | 音符打开 | |
Ovvvvvvv | 这个信息是在一个音开始时发出的,如键盘的某个键被按下,其中(nnnnnn)是音符音高的编号代码,C大调音高为“01100000”;(vvvvvvv)是力度的编号代码。 | ||
1010cccc | Onnnnnnn | 和弦(触后)键压力变化 | |
Ovvvvvvv | 这个信息是在先前被按下的琴键的压力变化时发出的,其中(nnnnnnn)是音符的编号代码,(vvvvvvv)是变化后新力度的编号代码。 | ||
1011cccc | 0ccccccc | 控制器变化 | |
0vvvvvvv | 这个信息是在某个控制器的控制值发生变化时发出的,例如:踏板等变化 | ||
1100cccc | 0ppppppp | 程序变化(乐器切换) | |
这个信息是在音色号码被改变时发出的,其中(ppppppp)是新的音色号码。GM标准音色号可在附录一中查询。 | |||
1101nnnn | 0ccccccc | 通道(触后)压力变化 | |
这个信息是在通道的压力发生变化时发出的,当有一些对力度敏感的键盘不支持上文中提到的和弦触后(Polyphonic Aftertouch)时,可以通过发送这个信息来改变当前所有被按下的键中力度最大的单个键的力度信息,其中(ccccccc)是控制代码。 | |||
1110nnnn | 0lllllll | (触后)音调轮变化 | |
0mmmmmmm | 这个信息的发送说明音调轮有变化 | ||
通道模式信息(Channel Mode Message) | |||
1011nnnn | 0ccccccc | 通道模式信息 | |
0vvvvvvv | 通道模式消息是“控制器消息”中的特殊情况,特定的控制器类型被作为保留字段识别为该类型信息。 通道模式
所有音符关闭: |
附录五:控制器类型表
MIDI控制器一览表 | |
控制器编号 | 参数意义 |
0 | 音色库选择MSB |
1 | 颤音深度(粗调) |
2 | 呼吸(吹管)控制器(粗调) |
3 | N/A |
4 | 踏板控制器(粗调) |
5 | 连滑音速度(粗调) |
6 | 高位元组数据输入(Data Entry MSB) |
7 | 主音量(粗调) |
8 | 平衡控制(粗调) |
9 | N/A |
10 | 声像调整(粗调) |
11 | 力度控制器(粗调) |
12-15 | N/A |
16-19 | 一般控制器 |
20-31 | N/A |
32 | 插口选择 |
33 | 颤音速度(微调) |
34 | 呼吸(吹管)控制器(微调) |
35 | N/A |
36 | 踏板控制器(微调) |
37 | 连滑音速度(微调) |
38 | 低位元组数据输入(Data Entry LSB) |
39 | 主音量(微调) |
40 | 平衡控制(微调) |
41 | N/A |
42 | 声像调整(微调) |
43 | 力度控制器(微调) |
44 | 效果FX控制1(微调) |
45 | 效果FX控制2(微调) |
46-63 | N/A |
64 | 保持音踏板1(延音踏板) |
65 | 滑音(在音头前加入上或下滑音做装饰音) |
66 | 持续音 |
67 | 弱音踏板 |
68 | 连滑音踏板控制器 |
69 | 保持音踏板2 |
70 | 变调 |
71 | 音色 |
72 | 放音时值 |
73 | 起音时值 |
74 | 亮音 |
75-79 | 声音控制 |
80-83 | 一般控制器(#5-#8) |
84 | 连滑音控制 |
85-90 | N/A |
91 | 混响效果深度 |
92 | (未定义的效果深度) |
93 | 合唱效果深度 |
94 | (未定义的效果深度) |
95 | 移调器深度 |
96 | 数据累增 |
97 | 数据递减 |
98 | 未登记的低元组数值(NRPN LSB) |
99 | 未登记的高元组数值(NRPN MSB) |
100 | 已登记的低元组数值(RPN LSB) |
101 | 已登记的高元组数值(RPN MSB) |
102-119 | N/A |
120 | 关闭所有声音 |
121 | 关闭所有控制器 |
122 | 本地键盘开关 |
123 | 关闭所有音符 |
124 | Omni模式关闭 |
125 | Omni模式开启 |
126 | 单音模式 |
127 | 复音模式 |
附录六:Note Key 与音阶的对应关系
音阶# | Note Keys | |||||||||||
| C | C# | D | D# | E | F | F# | G | G# | A | A# | 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 |
|
|
|
|
参考列表太长,就算了吧,所有引用已在原文中注明出处,若存在版权问题烦请与博主联系。谢谢。