注:关于 velocity 可以参考附录的介绍。
NOTE OFF 消息和 NOTE ON 消息基本一样: Status byte : 1000 CCCC
Data byte 1 : 0PPP PPPP
Data byte 2 : 0VVV VVVV
其中 CCCC 和 PPPPPPP 含义同上。VVVVVVV 是释放速率,可以看作是按键抬起的速度,这个值很少使用,通常将其设置为零。另外,在实践中经常使用速率为 0 的 NOTE ON 消息取代 NOTE OFF 消息。 需要额外说明的是 MIDI 协议还提供了一组 All Notes Off 消息,当某个信道接收到 All Note Off 消息之后会关闭所有还在发音的振荡器,通常来说 All Notes Off 消息用于在演奏、播放结束后用于清理状态,这里不多赘述。 2.2 乐器选择乐器选择消息的格式如下: Status byte : 1100 CCCC
Data byte 1 : 0XXX XXXX
其中唯一的一个 DATA byte 表示乐器编号,支持 128 个不同的乐器。由于不同的软件上存在的乐器音源并不一致,为了让 A 设备上创建的标准 MIDI 文件在 B 设备上播放时听起来相似,乐器厂商边采用 General MIDI 协议来编排音源。Gerneral MIDI 通常简写为 GM ,它提供了一个标准化的音库,将 128 个乐器排列成 16 个系列,每个系列有 8 个同类型的乐器,并为每个乐器分配一个特定的程序编号。GM 乐器表可以参考: http://www.harfesoft.de/aixphysik/sound/midi/pages/genmidi.html 在 GM 标准下,信道 10 是保留给打击乐器的(实际上合成器可以在任何信道上使用鼓),在这个信道上乐器编码遵循通用 MIDI 鼓乐器列表(General MIDI drum instruments list),具体可以参考: https://en.wikipedia.org/wiki/General_MIDI#Percussion 由于鼓是总体上是噪音乐器,所以之前的音高参数则被映射为不同的鼓音效。 注:噪音乐器指没有明确音高的乐器,有明确音高的乐器称为乐音乐器。 2.3 控制器消息MIDI 设备通常会提供一些控制器用于改变合成器的某个参数,比如混响、增益等。MIDI 协议可以使用控制器消息操作 128 个不同的控制器,控制器消息结构如下: Status byte : 1011 CCCC
Data byte 1 : 0NNN NNNN
Data byte 2 : 0VVV VVVV
其中 NNN NNNN 是控制器的编号,VVV VVVV 则是控制器的值。 控制器消息一方面可以用于改变合成器的某些参数,比如我们可以用以下指令将某个信道的力度值设置为 100: Status byte : 1011 CCCC
Data byte 1 : 0000 0111
Data byte 2 : 0110 0100
另一方面,控制器编码可以通过“组合”的方式实现一些更复杂的指令。如前文所述,选择乐器可以通过 1000 开头的 STATUS byte 实现,这个指令可以选择 128 种乐器。对于同一个乐器来说可以应用不同的音色库,比如我可以在钢琴上使用雅马哈的采样、施坦威的采样或者是珠江的采样,由于乐器厂商认为 128 这个数量对于音色库太小了,所以采用的 MSB + LSB 的方式表示音色库,例子如下: Status byte : 1011 CCCC
Data byte 1 : 0000 0000 // 0 = Sound bank selection (MSB)
Data byte 2 : 0000 0101
Status byte : 1011 CCCC
Data byte 1 : 0010 0000 // 32 = Sound bank selection (LSB)
Data byte 2 : 0000 0001
Status byte : 1100 0000
Data byte 1 : 0000 0010
这段代码选择了一个编号为 2,并且音色编号为 MSB = 0, LSB = 32 的乐器。由于 MSB 和 LSB 的范围都是 2 ^ 7 = 128,所以理论上可以选择的音色为 (2 ^ 7) ^ 2 = 16384 在 MIDI 中控制器消息和音源与效果器的参数密切相关,不同编号的控制器有一些约定俗称的含义,在程序中实现控制器时尽量与已有的规范对齐,具体内容可以参考这个表格:MIDI CC List(https://professionalcomposers.com/midi-cc-list/) 注:MSB 指最高有效字节(most significant byte),LSB 指最低有效字节(least significant byte)。一个 14 位的数据 XXX XXXX YYY YYYY 可以用 MSB + LSB 表示为:0XXX XXXX 0YYY YYYY 2.4 弯音消息弯音消息也用到了我们刚才提到的 MSB + LSB 表示法,其消息结构如下: Status byte : 1110 CCCC
Data byte 1 : 0LLL LLLL
Data byte 2 : 0MMM MMMM
其中 LLL LLLL 表示 LSB,MMM MMMM 表示 MSB,弯音值 0x2000(即 0b10000000000000)为同音高,0x3FFF(即 0b11111111111111)表示上方大二度,0x0000(即 0b00000000000000)表示下方大二度。在实践中,我们可以通过连续发送递增或者递减的弯音消息来表现滑音。 2.5 系统独占消息所有系统消息都以 1111 开头,其中有两个特殊的消息。一个是 1111 0000 它表示后面的消息是系统独有的。另外一个 1111 0111 则表示系统独有消息结束,消息结构如下: 11110000
0iiiiiii
0ddddddd
..
..
0ddddddd
11110111
当合成器监听到 1111 0000 时,检查下一个字节 0iii iiii , iii iiii 是一个 7 位的制造商 ID。如果合成器识别出这个代码则会继续监听后面的数据,否则则忽略掉收到的消息,直到结束消息 1111 0111 出现。 3. 宿主的 MIDI API许多宿主环境都提供了用于编写 MIDI 交互程序的 API,在浏览器上是 Web MIDI API,在 iOS & Mac 上是 Core MIDI,Android 上则有 AMidi。为了方便读者进行实际操作,我们以 Web MIDI API 为例展示如何编写一个最基本的 MIDI 程序: const button = document.getElementById('console-message')
button.addEventListener('click', () => {
if (navigator.requestMIDIAccess) {
navigator.requestMIDIAccess()
.then(success, failure);
}
})
function success (midiAccess) {
const inputs = midiAccess.inputs.values();
for (let input of inputs) {
input.value.onmidimessage = onMIDIMessage;
}
}
function failure () {
console.error('No access to your midi devices.')
}
function onMIDIMessage (messageEvent) {
console.log(messageEvent)
}
在这里,我们可以通过 requestMIDIAccess 向用户索要访问 MIDI 设备的权限,用户允许后我们会拿到一个 midiAccess 对象,可以通过这个对象拿到所有的输入和输出设备。我们可以通过设备对象提供的 onmidimessage 回调监听 midi message。 MIDI 消息的编码存储在 messageEvent 的 data 成员中,通过打印出的信息我们可以发现 Web MIDI API 并不会省略 Status Byte,这是为了便于开发者更容易区分指令属于哪个状态,而不必手动保存 MIDI 的运行状态。 如果想要 MIDI 可以发音,我们可以使用 Web Audio API 提供的振荡器: const button = document.getElementById('play-sound')
const oscillators = {};
let context
button.addEventListener('click', () => {
context = new AudioContext()
if (navigator.requestMIDIAccess) {
navigator.requestMIDIAccess()
.then(success, failure);
}
})
function success (midiAccess) {
const inputs = midiAccess.inputs.values();
for (let input of inputs) {
input.onmidimessage = onMIDIMessage;
}
}
function failure () {
console.error('No access to your midi devices.')
}
function onMIDIMessage (message) {
const frequency = midiNoteToFrequency(message.data[1]);
// midi 键盘的普通按键默认使用通道 0,所以其 note on 事件为 1100 0000
if (message.data[0] === 144) {
playNote(frequency);
}
// note off
if (message.data[0] === 128) {
stopNote(frequency);
}
}
function midiNoteToFrequency (note) {
return Math.pow(2, ((note - 69) / 12)) * 440;
}
function playNote (frequency) {
oscillators[frequency] = context.createOscillator();
oscillators[frequency].frequency.value = frequency;
oscillators[frequency].connect(context.destination);
oscillators[frequency].start(context.currentTime);
}
function stopNote (frequency) {
oscillators[frequency].stop(context.currentTime);
oscillators[frequency].disconnect();
}
我们可以使用这个小程序来回顾与验证我们之前讲到的 MIDI Message 知识。 这里有一个笔者以前做的视唱练耳小工具,可以使用 MIDI 键盘进行视唱练耳练习: 演示地址:muse-training(https://muse-training-8gwn0lc039762917-1252681582.tcloudbaseapp.com/) 仓库地址:https://github.com/lipd/muse-training
|