870920 Menu

SwingCoder之数字音频与DSP编程基础·2

MIDI编程基础

MIDI编程需具备良好的十六进制基础,并熟练掌握Bit、Byte等概念与C/C++语言的位运算。MIDI的本质是计算机和MIDI设备的通信,即字节序列的发送和接收。序列中的数据以MIDI消息为单位,1条MIDI消息由3个字节构成,分别为:

状态字节:用于定义MIDI消息的类型,数值范围10000000到11111111,对应十六进制0x80到0xFF。
数据字节1:紧随状态字节,其第一位始终为0,因此有效数值范围为0x80到0x7F(0到127)。
数据字节2:紧随数据字节1,第一位也始终为0,用于标示数据字节1的具体数值。
MIDI消息可分为7种类型,按状态字节的高位字节数进行划分:
消息类型 说明 状态字节 数据字节1 数据字节2
Note Off 音符停止发声 0x80 – 0x8F 音符编号(0到127) 力度值(0到127)
Note On 音符发声 0x90 – 0x9F 音符编号(0到127) 力度值(0到127)
Poly Key Pressure 复音触后 0xA0 – 0xAF 音符编号(0到127) 压力值(0到127)
Controller Change 控制器改变 0xB0 – 0xBF 控制器编号(0到127) 控制值(0到127)
Program Change 音色改变 0xC0 – 0xCF 音色编号(0到127) N/A
Channel Pressure 通道压力 0xD0 – 0xDF 压力值(0到127) N/A
Pitch Bend 弯音 0xE0 – 0xEF 粗调值(0到127) 精调值(0到127)
表 1 13 MIDI消息的类型,状态字节和数据字节

其中,Note On音符开消息的0x9X中的高位字节9代表音符开,而低位字节0到F则代表该消息所位于的MIDI通道(0到F共代表16个MIDI通道,0x90代表第一通道的音符开消息)。一个典型的3字节MIDI消息为:
0x92 0x40 0x51
可读为:第3通道音符发声,E4音符(编号为60号,即十六进制的0x40),力度值为81(对应十六进制的0x51)。也可从右至左进行解读:力度值为81的E4音符开始发声,该音符位于第3通道。
MIDI消息有个非常重要的概念是“运行状态”,即:如果当前消息的类型(状态字节)与上一条消息的类型(状态字节)一致,则不再重复发送状态字节。也就是说:MIDI消息的类型具有后延性。这样一来,可大大提高传输效率。采用“运行状态”来发送和接收MIDI消息,每秒可处理1000条以上。通常,音符停止发声的状态字节不使用0x8X这个数值范围,而是使用音符发声的状态字节0x9X,同时将音符的力度值设置为0。大多数MIDI编程均采用这种方式。如此一来,配合“运行状态”技术,可进一步提高效率。如果坚持使用0x8X来停止音符,则该消息的数据字节2力度值代表音符停止发声时的力度,即释放时的力度值。
注意:力度值并不绝对代表音符发声的响度,有些厂家甚至将某些音色的力度响应定义为LFO的调制比率。
关于复音触后(Poly Key Pressure):即通道触后,同时按下多个音符,复音发声时,其中某个或某些音符可单独执行力度触后,此谓复音触后。对应的则是单音触后(影响所有同时发声的音符)。单音触后和通道触后不同,通道触后仅影响特定通道的所有音符,单音触后则影响所有通道中同时发声的音符。
关于控制器改变(Controller Change):简称CC,常用的CC有:
控制器编号 作用 控制器编号 作用
CC 0 选择(切换)音色库 CC 1 调制轮
CC 2 呼吸控制器 CC 4 脚踏控制器
CC 6 数据登录(MSB) CC 7 音量
CC 10 声像 CC 11 表情
CC 64 延音踏板 CC 71 声音控制器2(木管音色/谐波强度)
CC 74 声音控制器5(亮度,“铜管味”) CC 91 混响效果
CC 93 合唱效果 CC 120 通道模式消息,所有声音停止发声
CC 121 通道模式消息,重置所有控制器 CC 123 通道模式消息,所有音符停止发声
表 1 14 MIDI 1.0常用控制器及其作用
系统消息:字节数据范围:0xF0到0xFF。系统消息是全局消息,影响整个系统,不包含与通道有关的数据,可实现多种用途,比如设备之间的同步,或发送专用的消息,等等。
 F0:系统专用消息开始
 F1:MIDI时间码。绝对时间,与乐曲速度无关,时分秒帧的格式,用于MIDI设备之间的同步
 F2:乐曲位置指针。相对时间,以每四分音符24个嘀嗒为单位记录本次播放至今所用的时间
 F3:乐曲选择
 F6:调音请求
 F7:系统专用消息结束
 F8:计时时钟。用于两个MIDI设备之间的精确同步(每个四分音符24个嘀嗒)
 FA:序列开始。通知MIDI设备或音序器,从当前乐曲的最开头处播放
 FB:序列继续。通知MIDI设备或音序器,从上次停止时的位置开始播放
 FC:序列停止。通知MIDI设备或音序器,停止播放
 FE:连接确认。当不发送其它系统消息时,自动发送此消息,用于确认两个设备之间是否依然连接
 FF:系统重置。通知MIDI设备或音序器,重置所有控制器(恢复为默认值)
注意:系统消息的字节数值中无F4、F5、F9、FD。

 每个MIDI音符均有一个唯一性编号,钢琴键盘上的中央C对应60号MIDI音符(C4)
 由于标准A(69号MIDI音符)的音高频率为440Hz,因此可推出中央C的音高频率为261.626Hz
 中央C以下有5个八度音域,以上有将近6个八度。中央C的标记为“C5”
 0~127号MIDI音符共涵盖了10个八度,外加8个音符
 一个八度由12个半音(小二度)构成,中央C降低一个八度,对应的是48号MIDI音符
 每升高一个八度,音高频率 * 2。每降低一个八度,音高频率 / 2(或乘以0.5)
 由上可知:半音变化的系数为:1.0594630943592952645618252949463。即:2的12次方根,也可在程序中用表达式精确的表示半音系数:
double semitRatio = pow (2.0, 1 / 12.0);
 某个音符的升小二度的音高频率为:该音符的频率 * 系数
 半音系数的12次方约等于2.0,相当于升高一个八度,跨越了12个半音
 如果当前调式并非基于12音律的半音体系,则半音系数计算公式为:
double semitRatio = pow(2.0, 1.0 / 几音律);
 最低音C0的音高频率为:8.2070625Hz,这已经远远超出人类的听阈范围了
 计算某个音符的音高频率:
/** 基于标准A音(音符编号:69)的音高频率,返回1参MIDI音符所对应的音高频率 */
double getFreq(const int noteID, const double freqOfA = 440.0)
{
return freqOfA * pow(2.0, (noteID – 69) / 12.0);
}
 SMF:标准MIDI文件(Standard MIDI File),二进制文件,扩展名为“.mid”。格式1为通道分轨,格式0为所有通道集中为一轨。
 采用格式1的MIDI文件,其每一轨通常以这些MIDI消息作为其参数:Program Change, Volume, Pan, Reverb, Chorus等等。
 时间戳(Time Stamp):每个MIDI消息均有一个时间戳信息,该信息标示了本条消息所执行的时间点,亦即本条消息在MIDI序列中的“位置”。
 Tempo(速度元事件):每个四分音符的时值为多少毫秒。不给出则默认500毫秒(对应120 BPM)。
 BPM(音乐速度):每分钟几拍(每分钟几个四分音符),此值与拍号无关。Tempo速度元事件转换为BPM的公式为:BPM = 60,000 / Tempo
 PPQ(时基):每个四分音符的嘀嗒值。嘀嗒值与乐曲的速度值无关,比如:可以使用120 PPQ嘀嗒值播放速度为100 BPM的乐曲,也可以使用480或960 PPQ的嘀嗒值来播放同一个乐曲。
 MIDI时钟(Clock):用于两个设备之间的播放同步。每个四分音符有24个MIDI时钟。比如:一个设备按其当前速度生成MIDI时钟,另一个设备收到该时钟后,以此为时间单位进行同步。MIDI时钟与SMPTE不同,MIDI时钟发送的字节数据是乐曲速度的相对比率。可以按PPQN嘀嗒值计算MIDI时钟,如果每个四分音符为96 PPQN,则MIDI时钟则为96 / 24 = 4,即每四个PPQN出现一个MIDI时钟。
 SMPTE(时间码):单位为:“时:分:秒”,允许将秒分为更小的单位“帧”,有4种不同的帧率:每秒24、25、29或30帧,每一帧又可以细分为subframes(子帧)。SMPTE不直接关联到乐曲速度,也不随乐曲速度而改变。
 MIDI文件可使用两种时间格式:SMF:每个四分音符多少个PPQ(嘀嗒);SMPTE:每秒多少个PPQ。某个MIDI文件具体使用了哪种时间格式,可使用JUCE类库中MidiFile::getTimeFormat()函数来获取,其返回值为正数,代表使用了SMF格式,返回值为负数,则代表使用了SMPTE格式。同理,写入MIDI文件时,可使用MidiFile::setTicksPerQuarterNote()或者MidiFile::setSmpteTimeFormat()设置其时间格式。
MIDI速度、时基与时间码的计算公式:
 BPM = 60,000 / Tempo(MIDI文件中的速度元事件)
 每个PPQN的毫秒时间 = Tempo / PPQN(960,480,120等等)
 每个MIDI时钟的毫秒时间 = Tempo / 24
 每个MIDI时钟对应的PPQN = PPQN / 24
 每个四分音符的子帧时间 = Tempo / (帧率 * 子帧率)
 每个PPQN的子帧时间 = 每个四分音符的子帧时间/嘀嗒时基
 每个PPQN的毫秒时间 = 每个PPQN的子帧时间 * 帧率 * 子帧率
关于MIDI消息与MIDI事件(JUCE类库的硬性规定和内部做法):
 MIDI消息和MIDI事件是两个概念。MIDI消息无时间戳,MIDI事件则是带有时间戳的MIDI消息
 外部的MIDI数据在读入内存时就已忽略(丢弃)其原始时间戳信息
 JUCE内部为每个MIDI消息添加“位置”信息,该信息相当于无单位的时间戳