音乐研发必备:理解 MIDI 协议与标准 MIDI 文件格式

动手点关注 干货不迷路 👆

1. MIDI 简介

MIDI 协议即数字音乐接口(Musical Instrument Digital Interface),是电子乐器、合成器等演奏设备之间的一种即时通信协议,用于硬件之间的实时演奏数据传递。MIDI 协议诞生之初希望解决的事情是通过统一通信协议让不同乐器制造商的设备可以互相兼容,比如把 Roland 键盘接入 Yamaha 合成器。MIDI 协议的编码经过拓展后也可以作为一种记录音乐信息的文件格式,被称为“标准 MIDI 文件格式”。

在音乐技术研发中除了需要与音频打交道之外,许多场景中还需要直接处理音符信息。如果说 wav 与 mp3 记录的是音乐的物理现象,那么 MIDI 协议与 MIDI 文件则记录的是音乐这门语言的“文字”。本文的目的是让开发中涉及到音乐“本体”的同学可以了解这一最通用的演奏信息交互和文件存储格式的编码规则。同时通过对 MIDI 事件流等概念的认识,能在开发中更好地抽象自己的业务逻辑。

1.1 MIDI 数据流 & 编码

和 HTTP 这类协议不同,MIDI 作为传输协议时所有传递的信息都需要被实时响应,比如一个触键信息、一个效果器参数的改变都需要立刻被执行,所以其采用数据流的方式进行数据传输。MIDI 定义了一个 8 位的二进制数据流,许多时候我们可以使用 ASCII 码来将其表示为 16 进制的字符用于传输和保存。

对于 MIDI 标准文件格式来说,其存储的内容也是 MIDI 产生的事件流。一段典型的 MIDI 文件长这样:

4D 54 68 64 00 00 00 06 00 01 00 03 01 E0 4D 54
72 6B 00 00 00 1A 00 FF 03 03 31 32 33 00 FF 51
03 08 7A 23 00 FF 58 04 04 02 18 08 00 FF 2F 00
...

上面这个例子可能会造成一些困惑,因为 MIDI 文件确实对人类阅读不太友好,但其编码规则实际上是较易掌握的,下面我们就来逐步认识 MIDI 的编码规则。

注:在本文中,一个字节的最低有效位为第 0 位,最高有效位是第 7 位。比如在 X000 000Y 中,X 为第 7 位,Y 为第 0 位。

1.2 MIDI 消息

MIDI 最核心的功能是用于传输实时的音乐演奏信息,这些信息本质上是一条条包含了音高、力度、效果器参数等信息的指令,我们将这些指令称之为 MIDI 消息(MIDI message)。一条 MIDI 消息通常由数个字节组成,其中第一个字节被称为 STATUS byte,其后面有跟有数个 DATA bytes。STATUS byte 第七位为 1,而 DATA byte 第七位为 0。

开头的 STATUS byte 有两个作用:一个作用是表示系统或者某个信道状态的改变,其二个作用是确定当前 MIDI Message 的类型,MIDI 类型会确定后面 DATA byte 的数量和意义。这样说比较空洞,下面我们举一个例子:

Status byte : 1100 CCCC
Data byte 1 : 0XXX XXXX
Status byte : 1001 CCCC
Data byte 1 : 0PPP PPPP
Data byte 2 : 0VVV VVVV

第一个 STATUS byte 告诉我们这是一个进行乐器选择的 MIDI Message(1100 为乐器选择指令,CCCC 是信道编号)。乐器选择的 MIDI Message 只有一条 DATA byte,而这条 DATA Byte 的数据表示选择的乐器编号。第二条 1001 开头的 STATUS byte 则告诉我们这是一条 Note On 类型 MIDI message,这个类型按照约定有两个 DATA byte。

除了向整个系统发送的 MIDI 消息, STATUS byte 通常包含了信道编号(即例子中的 CCCC),16 个信道分别从 0000 到 1111。而向整个系统发送的 MIDI 信息则以 1111 开头,原来的信道编号变成了指令编号(比如播放指令:1111 1010,终止指令:1111 1100)。

需要注意的是,许多时候我们会连续发送许多相同状态的 MIDI 消息,这个时候可以省略 STATUS byte,合成器会沿用最后一个接收的 STATUS byte,被合成器记录的状态称之为 MIDI RUNNING STATUS。

总结一下:

  • 一条 MIDI message 由 STATUS byte 和 Data byte 构成。

  • STATUS byte 以 1 开头,DATA byte 以 0 开头。

  • STATUS byte 确定消息的类型。后面的 DATA 字节数取决于消息的类型。

  • STATUS byte 通常包含信道编号,除了面向系统发送的指令。

  • 连续相同的 STATUS byte 可以省略。

2. 常用 MIDI Message

MIDI Message 不需要全部掌握,需要的时候可以直接到 MIDI 标准中查询,日常开发中只需要了解常用的几种 MIDI Message 即可。下面笔者介绍最常用的几种 MIDI Message。

2.1 NOTE ON & NOTE OFF - 音符的触发与终止

NOTE ON 和 NOTE OFF 是最主要的两个 MIDI Message。当演奏者敲击音乐键盘的琴键时发送 NOTE ON 消息,它包含了音高以及“力度”的参数。当合成器收到此消息时,它会开始以相应的音高和“力度”播放该音符。当收到 NOTE OFF 消息时,合成器会终止该音符。

每个 NOTE ON 消息都需要相应的 NOTE OFF 消息,否则该音符将一直处于播放状态。但打击乐器可以只发送 NOTE ON,因为打击乐音符会自动停止。但最好养成始终发送 NOTE OFF 的习惯,因为不同合成器对这一特性的实现可能不一样。

下面我们举例说明 NOTE ON:

Status byte : 1001 CCCC
Data byte 1 : 0PPP PPPP
Data byte 2 : 0VVV VVVV

在这个例子中,1001 可以理解为 NOTE ON 事件的编码,CCCC 是信道编号。

PPP PPPP 表示音高值,在 General MIDI 协议中(后文会提到),通常使用 69 表示标准音 A4(440 Hz),音高值增减一,就增减一个半音。比如 60 表示 C4(中央 C), 61 表示 C#4。同样,升高或者降低八度只需要在当前音高上增减 12 即可。

VVV VVVV 表示速率(velocity),这个速率可以理解为敲击键盘的速度,或者管乐器气流的速度。在最基础的合成器中,速率仅用于确定弹奏音符的力度,唯一的效果是音符音量变大或变小。总体来说,下面这张表可以作为速率和乐谱中的力度记号的对应关系参考:

356a85fb7f665a236c568bf968b4d8ad.png

但在在一些复杂的仿真建模合成器中,速率也会影响音色。我们以 Galaxy Steinway 采样器为例:

739745031628382c247dab8e455836f0.png e56b152beb4572599eeb4e3089ceec15.png

图片来源:https://zhuanlan.zhihu.com/p/19964066

左侧是小力度敲击的频域图,右侧是大力度敲击的频域图。我们可以看到大力度敲击不仅产生了更多的泛音,也在低频区产生了一些噪音(木材被撞击的声音)。

注:关于 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 iiiiiii 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

82f958fb950b01900506496f5f7394f8.png

4. 标准 MIDI 文件格式规范

MIDI 协议解决的是音乐设备之间的即时通讯问题,它本质上是一个硬件之间的通信协议。而当我们想把 MIDI 演奏保存在磁盘上则需要用到标准 MIDI 文件格式规范(Standard MIDI-File Format Spec)。和 MIDI 通信协议一样,MIDI 文件也是 8 位字节流,下文将会说明 MIDI 文件一些最基本的格式规范。

4.1 Chunk

Chunk 是构成 MIDI 文件的基本单元。一个 Chunk 由三个部分组成:Chunk 类型 、Chunk 长度以及 Chunk 数据。Chunk 类型是 4 个 ASCII 字符,之后使用 32 位表示 Chunk 数据的长度,最后才是 Chunk 需要存储的数据。

MIDI 中一共有两种 Chunk,分别为 Header Chunk 和 Track Chunk。Header Chunk 标记为 MThd,存储的是整个 MIDI 文件的基本信息,和 PNG 等文件的 Header Chunk 类似。Track Chunk 标记为 MTrk,每个 Track Chunk 都存储了一个 MIDI 事件流,一个事件流可以包含 16 个 MIDI 信道的消息。一个典型 MIDI 文件的结构如下:

MThd <length>
<MThd data>
MTrk <length>
<MTrk data>
MTrk <length>
<MTrk data>
3919c315acd24896d408df88884c8aec.png

4.2 Header Chunk

MIDI 文件的 Header Chunk 包含的信息非常简单,我们以上面这个文件为例:

4D 54 68 64    // MThd 的 ASCII 码
00 00 00 06    // MThd 的数据长度,MThd Data 固定为 6 字节
---- DATA 部分 ----
00 01          // MIDI 文件格式,有 0、1、2 三种
00 02          // MIDI 文件的包含的音轨数量,即 Track Chunk 数量
00 DC          // MIDI 文件的时间类型

前两条数据已经介绍过,这里不再赘述。我们来解释一下 MIDI 文件格式与 MIDI 时间类型:

MIDI 文件格式(MIDI File Formats)

MIDI 文件格式分为三种,格式 0 的 MIDI 文件只有一个 Header Chunk 和一个 Track Chunk。对于只有一个轨道的程序可以采用这种格式。

格式 1 有一个 Header Chunk ,和多个 Track Chunk 。其中第一条 Track Chunk 是特殊的,负责记录 MIDI 文件的所有 Meta Event(后面会讲到),而从第二条 Track Chunk 开始才会记录 MIDI Event,所以我们上图中的 MIDI 文件实际上只有一条用于演奏的音轨。目前绝大部分的支持多音轨的程序都采用这种格式,笔者也建议读者尽量使用这种格式。

格式 2 的 MIDI 文件也有多个 Track Chunk,但不同的是格式 1 所有 Track Chunk 共用一条时间轴,所有 Track 应当被视作同时播放的。而格式 2 中 Track Chunk 都有自己独立的时间信息,这种格式非常少见,不建议使用。

我们用一张表总结一下:


音轨数量时间轴
格式 01 个1 条
格式 1多个1 条
格式 2多个多条

MIDI 时间类型

MIDI 时间类型主要有两种,为了方便介绍读者可以简单将其理解为“按音符分割的”和“按帧分割的”:

“按音符分割的”时间类型 15 位为 0,被称为 TPQN(Ticks Per Quarter-Note),即一个四分音符中包含了多少 Tick。在前文的例子中 00 DC 表示 TPQN 为 220,那么一个八分音符为 110 Ticks,一个二分音符为 440 Ticks。另外 TPQN 也被称为 Pulses Per Quarter-Note (每四分音符的脉冲数),如果你在代码中看到 PPQ、PPQN 这样的简写,你知道他们是一个意思即可。

“按帧分割的”时间类型 15 为 1,这种格式单纯 MIDI 文件中几乎不用而且比较复杂,建议读者跳过。其编码规则简单说就是使用了 SMPTE 时间码的规范。其 14 - 8 位包含了包含 -24、-25、-29 或 -30 四个值之一,对应于四种标准 SMPTE 时间码格式(-29 对应于 30 个丢帧),并表示每秒的帧数。第 7 到 0 位表示帧内分辨率。我们依然用一张表总结一下:


15 位14-8位7-0位
按音符0四分音符的Tick数
按帧1SMPTE格式每帧Tick数

4.3 Track Chunk

Track Chunk 的主要功能是用于存储实际的演奏数据。它的 Chunk Data 中存储的是一串事件流,被 Track Chunk 记录的事件我们称为 MTrk 事件,其结构如下:

<MTrk event> = <delta time> <event>

在这个结构中,事件可以指代三类事件:midi 事件、系统独有事件、元事件:

<event> = <midi event> | <sysex event> | <meta event>

delta time

MIDI 通信时所有信息都是即时执行,所以 MIDI 消息并没有记录时间,但是 MIDI 文件则需要记录时间在时间轴上的位置。MIDI 文件采用差量时间来记录 MIDI 事件,即 Δt。delta time 表示的是当前事件与上一个时间相差的 Tick 数。如果要表示同时发生的数个任务,则记录一串 delta time 为 0 的事件流即可。比如我们控制器一章中切换乐器的事件流可以表示为:

Delta time  : 0000 0000
Status byte : 1011 CCCC
Data byte 1 : 0000 0000   // 0 = Sound bank selection (MSB)
Data byte 2 : 0000 0101

Delta time  : 0000 0000
Status byte : 1011 CCCC
Data byte 1 : 0010 0000   // 32 = Sound bank selection (LSB)
Data byte 2 : 0000 0001

Delta time  : 0000 0000
Status byte : 1100 0000
Data byte 1 : 0000 0010

sysex event

即系统独占的消息事件,具体可以参考前文中的系统独占消息。

meta event

所有元事件以 1111 1111 开头,这个指令在 MIDI 消息中表示系统复位。这个指令是一个系统实时信息,通常在使用 MIDI 文件的程序并不会用到,所以在这里用于表示元事件。元事件主要用于指定拍号、调号、速度等。

需要注意的是FF 2F 00 是一个特殊的元事件,表示轨道结束。所有 Track Chunk 都以这个元事件结束。下面这张表是标准中已定义的元事件:


意义
FF 00 02序列号
FF 01 len text文本事件
FF 02 len text版权声明
FF 03 len text轨道名称
FF 04 len text轨道中使用的乐器类型
FF 05 len text歌词
FF 06 len text某个点的名称,比如“第一乐章”
FF 07 len textCue Point 某个舞台事件描述
FF 20 01 ccMIDI 通道前缀
FF 2F 00End of Track
FF 51 03 tttttt设置速度
FF 54 05 hr mn se fr ffSMPTE Offset
FF 58 04 nn dd cc bb拍号
FF 59 02 sf mi调号

5. MIDI 协议的缺陷与改良方案

5.1 MIDI 2.0 & MPE

MIDI 通信协议目前看来主要有两个较明显的缺陷。第一个缺陷是许多值可以表示的范围实在有限,比如 note off 的 velocity 就只有 128 个、乐器也只有 128 个、只有 16 个信道。

另一个问题更为麻烦,MIDI 中控制器、和弯音消息只能发送给某个信道,你根本就没法将它和某个音联系在一起。这一局限在以前并没有引起多少问题,因为传统乐器很少碰到按音处理控制器的情况。而弯音用得最频繁的更多是单声部乐器。

但电子音乐界向来不缺乏整活健将,工程师总是会想方设法突破现有限制。最典型的例子就是 seaboard 键盘,这玩意儿可以在每个键上提供弯音能力。你可以从下面这段演奏上感受到这一乐器的神奇魅力:

原视频链接:https://www.youtube.com/watch?v=6SCug5kUsBs

为了解决让控制器消息能按“音”发送,seaboard 的制造商 ROLI 制订了 MIDI Polyphonic Expression(MPE,MIDI 复音表示法)。其原理基本上可以概括为:让每个发声的音符都会在其 Note On 和 Note Off 之间临时分配一个 MIDI 通道。这样便把控制器消息和弯音消息与特定音符建立了联系,并且很好的兼容了 MIDI 协议。

上述问题现在都正在通过新的 MIDI 2.0 得到解决,在 MIDI 2.0 中 volocity 从 0 - 128 扩展到 0 - 65535,信道从 16 个增加到 256 个,同时 MIDI 2.0 也支持 MPE 以及远程控制。

5.2 如何拓展 MIDI

如果 MIDI 2.0 和 MPE 这类现成的解决方案无法满足你的需求,那么你可以考虑自己来拓展 MIDI 协议或者 MIDI 标准格式。目前来看,可靠的拓展方式有几下几种:

  1. 使用未定义的 MIDI 消息:比如系统消息 1111 0101 的行为在 MIDI 标准中就未被定义。这种方法的好处是不需要进行额外的解析工作,但缺点便是可以使用的指令十分有限。

  2. 使用自定义 Chunk:Chunk 在设计之初便考虑到了拓展的问题,你可以按照 Chunk 的格式自由地声明一个新的 Chunk 类型,主流解析工具在碰到无法解析的 Chunk 时会自动忽略掉,所以不用担心兼容的问题。如果你有整段的数据,既不属于 Track,又不能被 Heaer 所包含,那么可以考虑这种方式。

  3. 使用系统独占消息:如果你需要在 MIDI 通信协议上进行拓展,可以考虑使用系统独占消息,合成器会自动忽略无法解析的独占消息。具体可以参考附录中的系统独占消息一节。

  4. 其他:你也可以参考 MPE 的方式,基于现有的编码方案但是重新定义指令的意义和执行。

6. 思考与讨论

6.1 什么时候使用 MIDI 格式,什么时候不用?

首先我们需要认识到 MIDI 的优点,MIDI 记录的实际上是事件流,最适合的场景就是在现场演奏时用于硬件之间的通信。作为 MIDI 文件格式作为一种存储格式,其优点是数据十分紧凑,体积较小。但 MIDI 的缺点是十分明显的,一方面我们无法快速查询、访问其中某个具体内容的值:比如我们没法快速找到某一个轨道的拍号,或者某个音的音高。

所以我的建议是,尽量避免在现场演奏场景之外使用 MIDI 文件格式,但可以在抽象上对齐 MIDI。在内存中我们尽量把 MIDI 文件转化为实例对象,便于我们快速访问。在需要持久化的场景下则可以使用更容易解析的 JSON 或者 MusicXML 格式。只有在用户需要或者向其他编辑工具导出数据的时候,我们才考虑使用 MIDI 标准文件格式。

6.2 MIDI 协议无法满足的需求如何解决?

绝大部分这类问题可以通过不使用 MIDI 编码来解决。原则很简单,只要不涉及现场演奏场景和向其他工具导出数据,就避免使用 MIDI 编码来做任何事情。只用确保在需要 MIDI 的场景可以导出 MIDI 文件就行。

6.3 如果多数场景不使用 MIDI,那有必要深入学习 MIDI 协议吗?

如果你的开发工作涉及到音乐的“本体”部分,那么我建议多了解一些 MIDI 协议,因为虽然我们可能多数情况下不直接使用 MIDI 协议的编码,但是 MIDI 的事件流是创作场景和存储场景会大量用到的,同时 MIDI 中的多数抽象和概念是行业内通用的。

6.4 如何设计自定义的音乐数据格式?

我的建议是用一个文档维护所有的基础字段和拓展字段,各项目在定义 Model 时尽量参考这个文档。如果现有的拓展字段可以解决你的需求,就不要新增拓展字段。

附录

可变长度数量(Variable-Length Quantities)

由于单个字节表示的最大范围为 0 - 256,所以在 MIDI 文件中表示较大数字时会采用可变长度数量。其每一个字节使用第 7 位表示这个字节是否为最后一个字节,1 表示不是最后一个字节,0 表示是最后一个字节, 0 - 6 位则作为有效位。

举一个例子,数字 127 可以表示为 0111 1111 ,128 则表示为 1000 0001 0000 0000,这样理论上可以表示的数字可以无限大,不过在实践中通常不会使用超过 32 位。

总结一下就是:

7 位0-6 位
是否为最后一个字节有效位

速率的解释

note on 中的 velocity 实际上是按键的“触发速率”,你可以把其视为从键盘能感知到下按到下按结束这个过程中的键程除以按下时间,note off 则是反向的“释放速率”。速率的计算方式和更多细节可以参考这篇论文:The Interpretation of MIDI Velocity

一堆速查表

  • 查十进制的 MIDI 消息:Expanded MIDI 1.0 Messages List (Status Bytes)

  • 查 GM 乐器表:General MIDI Instrument List

  • 查 MIDI 事件流:Standard MIDI-File Format Spec. 1.1, updated

参考文献

  • https://www.midi.org/specifications

  • MIDI Tutorial

  • Standard MIDI-File Format Spec. 1.1, updated

  • MIDI Polyphonic Expression (MPE) Specification Adopted

  • 钢琴的触键方式是如何影响弹出来的音色的?(https://zhuanlan.zhihu.com/p/19964066)

  • MIDI Tick、Meta-event、变长数表示法、区分 MIDI 文件中单个字节的含义(https://www.cndzq.com/bbs/thread-117332-1-1.html)

  • MPE in Live 11(https://help.ableton.com/hc/en-us/articles/360019144999-MPE-in-Live-11)

  • GDX-620 使用说明书(https://de.yamaha.com/files/download/other_assets/9/334239/DGX-620_ZH.pdf)

推荐读物

  • 《音乐声学——音响、乐器、计算机音乐、MIDI、音乐厅声学原理及应用》- 龚镇雄

  • The Computer Music Tutorial - Curtis Roads

加入我们

字节跳动音乐研发团队,业务包含字节旗下的音乐流媒体应用、字节音乐中台、抖音 & 西瓜视频中的音乐视频和音乐创作工具等场景。团队拥有良好的技术氛围,在 ByteTech 沉淀了大量优秀的视频课程和技术文章,欢迎各位同学加入。

投递详情:字节跳动音乐研发团队热招,你的心动Offer已就位!

相关推荐

2024 抖音欢笑中国年(五):Wasm、WebGL 在互动技术中的创新应用

前言随着 Web 前端技术的不断发展,越来越多的新兴技术方案被引入到 Web 开发中,其中 Wasm 和 WebGL 作为前端领域的两大利器,为开发者带来了更多的可能性。本文将结合2024 年抖音欢笑中国年的部分项目,重点介绍如何利用 Wasm 和 WebGL 对目前流行的一些前端互动技术(比如 Lottie、渲染引擎、动画图片等)进行创新和实践,利用 Wasm 和 WebGL 等新技术方案的特性...

字节跳动技术团队官方博客 2万+

2024 抖音欢笑中国年(四):渲染技术实践与探索

作者:陈瑞、欧阳浩铸、王武俊、倪梵云前言抖音在2024年春节期间推出了欢笑中国年系列活动,为用户带来了全新的体验和乐趣。而SAR Creator则为该项目研发工作提供了重要的技术支持。SAR Creator是一款基于 Typescript 的高性能、轻量化的互动解决方案,目前支持了浏览器和跨端框架平台,服务于字节内部的各种互动业务。这些绚烂多彩的互动场景当然也离不开实时渲染技术的支持,因此本文将专...

字节跳动技术团队官方博客 2万+

2024 抖音欢笑中国年(三):编辑器技巧与实践

前言本次春节活动中,我们大部分场景使用内部的 SAR Creator互动方案来实现。SAR Creator 是一款基于 TypeScript 的高性能、轻量化的互动解决方案,目前支持了Web和字节内部跨端框架平台,服务于字节内部的各种互动业务,包括但不限于抖音春节、抖音直播礼物、抖音UG活动等。SAR Creator 编辑器支持了图形化界面,提供了各类完善的系统(光照、动画、脚本等)供用户快速便捷...

字节跳动技术团队官方博客 1万+

2024 抖音欢笑中国年(二):AnnieX互动容器创新玩法解析

本文基于24年抖音春节活动业务背景,介绍了字节跨端容器AnnieX在游戏互动套件上的探索,致力于提升容器在游戏互动场景的优化能力。业务背景AnnieX作为字节一方游戏统一容器,服务字节内部电商、直播、UG等跨端场景业务。在字节一方游戏互动场景,有大量的一方游戏业务对容器有特定的流量、端能力和游戏优化的诉求。因此我们不断深入互动游戏业务特点,为字节游戏提供完善游戏端能力和流量运营能力,同时提供游戏互...

字节跳动技术团队官方博客 1万+

2024 抖音欢笑中国年(一):招财神龙互动技术揭秘

字节跳动旗下的抖音等 App 在 2024 年春节期间推出了欢笑中国年系列活动,在实现增长业务目标的同时,为用户带来了全新的体验和乐趣。「招财神龙」是其中的一个重要玩法。前言本次春节活动,使用到了字节内的主要前端、跨端、互动技术产品。主要涉及:跨端框架提供了首屏直出的方案使其具有较短的首屏时间,能够大大提升业务加载成功率。跨端框架也提供了 Canvas 作为 SAR Creator 等渲染引擎的...

字节跳动技术团队官方博客 1万+

Monorepo 解决方案 — 基于 Bazel 的 Xcode 性能优化实践

背景介绍书接上回《Monorepo 解决方案 — Bazel 在头条 iOS 的实践》,在头条工程切换至 Bazel 构建系统后,为了支持用户使用 Xcode 开发的习惯,我们使用了开源项目 Tulsi 作为生成工具,用于将 Bazel 工程转换为 Xcode 工程。但是在使用的过程中,我们发现了一些问题,其中影响较大的是,Xcode 工程卡顿:对于头条这种大型项目来说,Xcode 卡顿一直是本地...

字节跳动技术团队官方博客 1万+

抖音 ANR 自动归因平台建设实践

背景介绍本文在 2024 年初最新一期『抖音客户端基础技术大揭秘』技术沙龙活动中已做过专题分享,本次将内容重新整理文章进行分享。公众号后台回复技术沙龙可查看沙龙回放及 PPT~抖音作为一个超大型的应用,我们在 ANR 问题治理上面临着很大的挑战。首先对于存量问题的优化,由于缺少有效的归因手段,一些长期的疑难问题一直难以突破解决,例如长期位于 Top 1 的 nativePollOnce 问题。同时...

字节跳动技术团队官方博客 2万+

CVPR 2024 | CAMixerSR 动态注意力分配的超分辨率加速框架

背景随着相关技术和应用的发展,比如超高清屏幕、虚拟现实(VR)等沉浸式体验的增加,用户对超高分辨率图像和视频的需求变得越来越强烈。在这些场景中,图像的质量和清晰度对于提供最佳的用户体验至关重要。超高分辨率不仅能提供更清晰、更真实的视觉效果,还能在一定程度上增强人们的互动和沉浸感,在一些VR场景中我们需要8K甚至16K的才可以满足需求。然而要生成或者处理这些超高分辨率的内容,对算力的要求也是与日增长...

字节跳动技术团队官方博客 1万+

CVPR 2024 | Modular Blind Video Quality Assessment:模块化无参视频质量评估

无参视频质量评估 (Blind Video Quality Assessment,BVQA) 在评估和改善各种视频平台并服务用户的观看体验方面发挥着关键作用。当前基于深度学习的模型主要以下采样/局部块采样的形式分析视频内容,而忽视了实际空域分辨率和时域帧率对视频质量的影响,随着高分辨率和高帧率视频投稿逐渐普及,特别是跨分辨率/帧率视频转码档位画质评估场景中,这种影响变得更加不可忽视。在本文中,我们...

字节跳动技术团队官方博客 1万+

2024 AI & 前端:回首展望,光芒未至,破晓之前!

前言回望 2023 年,ChatGPT 的突然爆火,让 AI 无疑成为最为值得注目的新兴领域之一,我们也一起见证了生成式 AI 的寒武纪大爆发。这一年来,国内外的生成式 AI 、大模型和相关产品以令人眼花缭乱的速度更新迭代,新的创业浪潮风起云涌。在这 AI 浪潮下,也让我们有了新的开发思考,探索着在各个环节中“前端 & AI”的应用场景。勇于探索的前端开发者们已经开始挥舞着 AI 的“魔法...

字节跳动技术团队官方博客 1万+

Kotlin 云端差分缓存技术

本文由字节跳动 Buildinfra 团队出品。在我们的工程上线 Monorepo 全源码后,Kotlin 编译成了整个编译中最耗时的步骤,全源码过程中大量的 BuildCache Miss 导致我们的编译数据落后原来多仓二进制时代很多,且业界没有相关的解决方案。本篇文章我们来具体阐述下 BuildInfra 团队自研的解决方案 - Kotlin 云端差分方案的原理和技术实现。一、Monorepo...

字节跳动技术团队官方博客 1万+

字节跳动基础架构SRE-Copilot获得2023 CCF国际AIOps挑战赛冠军

近日,2023 CCF国际AIOps挑战赛决赛暨“大模型时代的AIOps”研讨会在北京成功举办,活动吸引了来自互联网、运营商、科研院所、高校、软硬件厂商等领域多名专家学者参与,为智能运维的前沿学术研究、落地生产实践打开了新思路。决赛中,从初赛两百多支队伍中脱颖而出的十支入围队伍分别展示了各自的方案,并进行了现场答辩,评审专家从选题方向、创新性、实用性、完整度和实验复现结果等多角度进行了综合评定,最...

字节跳动技术团队官方博客 2万+

字节跳动百万级Metrics Agent性能优化的探索与实践

背景metricserver2 (以下简称Agent)是与字节内场时序数据库 ByteTSD 配套使用的用户指标打点 Agent,用于在物理机粒度收集用户的指标打点数据,在字节内几乎所有的服务节点上均有部署集成,装机量达到百万以上。此外Agent需要负责打点数据的解析、聚合、压缩、协议转换和发送,属于CPU和Mem密集的服务。两者结合,使得Agent在监控全链路服务成本中占比达到70%以上,对Ag...

字节跳动技术团队官方博客 1万+

西瓜视频RenderThread引起的闪退问题攻坚历程

背景影响西瓜之前存在过一类RenderThread闪退,从堆栈上看,全部都是系统so调用,给人的第一印象像是一个系统bug,无从下手。闪退集中在Android 5~6上,表现为打开直播间立即闪退。该问题在2022年占据Native Crash Top5,2023年更是上升到到Top1。因此有必要投入时间和精力再重新审视一下这个问题。在历经多周的源码分析和排查后,逐步明确了问题根因并修复,最终取得了...

字节跳动技术团队官方博客 2万+

字节电商双11 大促容量保障是如何做的?

前言Rhino 简介Rhino是字节自研全链路容量评估产品,致力于构建完整的全链路容量评估解决方案(覆盖:容量预估->资源准备->数据准备->容量验证->监控->分析->决策->处理反馈);围绕容量在稳定性、成本、效率 三方面提供业务全方位基础支撑。Rhino 目前已经成为字节各业务容量评估主流解决方案,并且历年来在业务大型活动稳定性保障中(抖音春节项目、...

字节跳动技术团队官方博客 9万+

使用火山引擎 APMPlus 解决抖音Top 1 Java 崩溃的通用优化方案

背景近3个月,抖音 Android 版面临一个多次触发线上报警的崩溃问题,全量版本和灰度版本的异常数据激增,该问题不仅容易触发报警,更成为了 Java Top 1 崩溃问题,带来巨大困扰,急需攻坚解决。本文展现了具体的分析过程、优化思路和解决方案,同时提供了已集成该方案的实用工具。初步分析多维特征我们以某发版期间数据为例进行分析:机型方面:比较分散,有聚集部分samsung sm-s9180 占比...

字节跳动技术团队官方博客 1万+

用 Addon 增强 Node.js 和 Electron 应用的原生能力

前言Node.js Addon 是 Node.js 中为 JavaScript 环境提供 C/C++ 交互能力的机制。其形态十分类似 Java 的 JNI,都是通过提供一套 C/C++ SDK,用于在 C/C++ 中创建函数方法、进行数据转换,以便 JavaScript / Java 等语言进行调用。这样编写的代码通常叫做 Bindings。此外还有基于 C ABI Calling Convent...

字节跳动技术团队官方博客 2万+

火山引擎 ByteHouse 的增强型数据导入技术实践

作为企业数字化建设的必备要素,易用的数据引擎能帮助企业提升数据使用效率,更好提升数据应用价值,夯实数字化建设基础。数据导入是衡量OLAP引擎性能及易用性的重要标准之一,高效的数据导入能力能够加速数据实时处理和分析的效率。作为一款OLAP引擎,火山引擎云原生数据仓库ByteHouse源于开源ClickHouse,在字节跳动多年打磨下,提供更丰富的能力和更强性能,能为用户带来极速分析体验,支撑实时数据...

字节跳动技术团队官方博客 1万+

打造企业级智能问答系统的秘密:如何使用云数据库 PostgreSQL 版实现向量检索

本文就如何利用火山引擎云数据库 PostgreSQL 版和大语言模型技术(Large Language Model,简称 LLM),实现企业级智能交互式问答系统进行介绍。通过本文,你将会了解交互式问答系统的原理,学习 PostgreSQL 的向量化存储和检索技术,以及大语言模型交互技术等。背景在大数据的浪潮下,众多企业建立了自己的知识库,以便于信息检索和知识查询。然而,随着知识库内容的膨胀,传统的...

字节跳动技术团队官方博客 2万+

抖音大型直播的画质优化实践

面临挑战随着抖音内容生态的不断丰富,越来越多的大型赛事在抖音平台进行直播,世界杯/春晚/亚运会等各项赛事节目引来大量用户观看。卡塔尔世界杯期间,抖音提供的稳定高质直播画面为观众带来了完美的观赛体验,决赛的 PCU 高达 3700W+。不同赛事节目涉及链路众多,且不同赛事之间存在差异,如何保障各链路的画质稳定并进一步提升画质,是一个巨大的挑战。如何应对挑战?画质优化链路大型赛事直播涉及链路较长,不同...

字节跳动技术团队官方博客 2万+

抖音直播新一代BVC编码器正式亮相

面临挑战在直播行业发展如火如荼的今天,用户对视频体验的要求也水涨船高。视频基础体验的关键要素包括清晰度、流畅度、低延迟等,而这些要素的“第一性原理”,就是视频本身的编码效率,也就是压缩率。视频编码是整个技术体系的基座,编码效率的显著提升,能够在同等码率下极大提高画质,从而改善用户体验。视频编码效率的重要性不言而喻,但进一步地提升也并非易事,尤其在直播场景中,对编码速度、延迟、码率控制等方面都有很高...

字节跳动技术团队官方博客 7322

Go Metrics SDK Tag 校验性能优化实践

背景Metrics SDK 是与字节内场时序数据库 ByteTSD 配套的用户指标打点 SDK,在字节内数十万服务中集成,应用广泛,因此 SDK 的性能优化是个重要和持续性的话题。本文主要以 Go Metrics SDK 为例,讲述对打点 API 的 hot-path 优化的实践。用户在使用 SDK API 进行打点时,需要传入指标对应的 Tag:tags:=[]m.T{{Name:"foo...

字节跳动技术团队官方博客 1万+

云上智能驾驶三维重建最佳实践

智能驾驶技术的不断发展,正在改变着我们的出行方式和交通系统。作为其中的一个关键技术,三维重建在智能驾驶系统中起着重要的作用。除去车端本身的感知、重建算法,自动驾驶技术的落地与发展需要庞大的云端重建能力支撑,火山引擎多媒体实验室通过行业领先的自研三维重建技术,结合强大的云平台资源与能力,助力相关技术在云端大规模重建、自动标注、真实感仿真等场景的落地与应用。本文重点介绍火山引擎多媒体实验室三维重建技术...

字节跳动技术团队官方博客 6289

火山引擎实时、低延时拥塞控制算法的优化实践

摘要火山引擎智能拥塞控制算法 VICC(Volcano Intelligent Congestion Control)是一种自适应的拥塞控制算法,旨在解决全球不同网络环境下,不同音视频应用对带宽利用率和延时的差异化要求。它结合了传统拥塞控制算法(如 GCC 和 BBR)的优点,并且能够根据不同的网络条件、业务偏好和码率特征进行自适应调整,包括自适应拥塞响应速度、自适应带宽探测幅度、自适应丢包检测策...

字节跳动技术团队官方博客 6709

veImageX 演进之路:Web 图片加载提速50%

背景说明火山引擎veImageX演进之路主要介绍了veImageX在字节内部从2012年随着字节成长过程中逐步演进的过程,演进中包括V1、V2、V3版本并最终面向行业输出;整个演进过程中包括服务端、客户端、网络库、业务场景与优化等多个角度介绍在图像处理压缩、省成本与体验优化的经验与方案;本篇文章重点介绍在web端演进和提供的能力,图片是 Web 站点中的重要元素,图片体积、格式、分辨率以及渲染方式...

字节跳动技术团队官方博客 6564

自研多模态追踪算法 PICO 为「手柄小型化」找到新思路

作者:张韬、林泽一 、闻超 、赵洋研发背景作为头戴的追踪配件,VR手柄可以通过HMD(头戴显示设备)的inside-out光学追踪定位原理,计算出手柄的空间运动轨迹,同时结合6轴传感器实现6DoF空间定位。与此同时,结合手柄控制器的物理按键、马达反馈、摇杆等,用户还能获得逼真、细腻的触觉反馈,进一步增强虚拟现实人机交互的能力以及沉浸感,这也是目前无手柄方案所难以实现的。目前主流VR手柄的追踪技术方...

字节跳动技术团队官方博客 7692

如何利用播放器节省20%点播成本

点播成本节省的点其实涉及诸多部分,例如:CDN、转码、存储等,而利用播放器降本却是很多客户比较陌生的部分。火山引擎基于内部支撑抖音集团相关业务的实践,播放器恰恰是成本优化中最重要和最为依赖的部分。火山引擎的视频团队做了份数据统计,在一个很经典的视频业务中,我们在2022年至2023年大约1年半的时间里,针对这个业务进行了33次成本优化点,其中13次是播放器主导的优化,其余的有12次也是需要播放器强...

字节跳动技术团队官方博客 6159

火山引擎 ByteHouse:ClickHouse 如何保证海量数据一致性

背景ClickHouse是一个开源的OLAP引擎,不仅被全球开发者广泛使用,在字节各个应用场景中也可以看到它的身影。基于高性能、分布式特点,ClickHouse可以满足大规模数据的分析和查询需求,因此字节研发团队以开源ClickHouse为基础,推出火山引擎云原生数据仓库ByteHouse。在日常工作中,研发人员经常会遇到业务链路过长,导致流程稳定性和数据一致性难保障的问题,这在分布式、跨服务的场...

字节跳动技术团队官方博客 6882

抖音集团都在用的画质评估工具,确定不试试吗?

导读本文从抖音集团内部画质评估体系的建设历程着笔,主要分享了画质评测对于业务的重要性、主要应用场景和内部产品的一些典型实践案例。通过分享业务视角遇到的一些问题和我们的解决思路,希望能抛砖引玉,为遇到类似困扰的伙伴们提供有价值的参考。画质评估体系建设历程为何评测画质如此重要?我们通过线上业务大量实验发现,图片画质优劣对点击率、 停留时长等消费类指标有正相关影响,间接影响用户收益指标。因此,建设一套行...

字节跳动技术团队官方博客 7900

VLDB 2023 | CDSBen: 字节跳动 veDB 数据库存储系统性能测试模型

背景随着业务爆炸式增长与云原生技术的日渐成熟,大量云原生分布式数据库产品如雨后春笋般涌现,其中一部分主打 OLTP 场景的分布式数据库强调的是从计算-存储分离架构获得弹性收益;对于业界各种计算-存储分离架构的数据库而言,怎么用真实的端到端数据库 workload 去 benchmark 其底层存储系统一直存在以下难题:对于数据库专用存储系统,不存在如 fio 一样的“事实标准” benchmark...

字节跳动技术团队官方博客 5882
上一篇: DanceNN:字节自研千亿级规模文件元数据存储系统概述
下一篇: 一文读懂全球化系统中的日期时间处理问题
 字节跳动技术团队
字节跳动技术团队 企业官方账号 企业官方账号
博客等级 码龄7年 8122粉丝 316原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值