浏览器中 MIDI 协议的探索与应用
很久以前就有的想法,我想把编曲宿主软件的 MIDI 事件输出到外部,然后就能根据它做一些音乐可视化的应用。
1.从宿主软件输出 MIDI 信号
宿主软件都是可以输出MIDI信号的,但需要一个中间软件转发才能被本机读取,这个软件就是 LoopMIDI。安装它打开后,按照下图创建端口。

注:MacOS 自带这个功能,直接启动台搜“音频 MIDI 设置”,怎么设置可以问AI.
创建完成后,就可以在宿主中选中输出的端口了,MIDI 协议最多创建 256 个端口(Port)。
以 FL Studio 为例,打开 选项 - MIDI 设置,在输出一栏,启用刚才创建的端口,并且为它绑定一个端口号 (例如 0 号端口)。

每个端口有 16 个通道,用于端口内的信号区分。
在通道机架上添加一个 MIDI Out,设置它将要输出信号的端口号 (例如 0 号端口)、通道 (例如 16 号通道)。 默认输出通道为 1,如果多个 Midi Out 向同一个端口输出,修改通道就很有必要了。或者也可以将音符颜色映射到通道。

这样一来,钢琴窗内的音符,便会通过指定的端口号、通道号、发送到信号到 LoopMIDI 了。
2.前端绑定
配置好宿主和 LoopMIDI 后,我们演示在浏览器中采用 WebMIDI.js 来读取信号(当然也可以在其他软件、宿主、游戏引擎中读取)。
先获取前面 LoopMIDI 中创建的端口:
// 先定义端口变量备用
let input;
// 启用 MIDI,会弹窗获取授权
WebMidi.enable().then((midi) => {
// 找到你要的端口(可以根据端口名称判断,这里演示直接取第1个)
input = midi.inputs[0];
})
然后就可以监听端口事件了:
// 监听 "音符按下" 事件
input.addListener("noteon", (event) => {
// 音符通道
let channel = event.message.channel
// 音符音高
let note = event.dataBytes[0]
// 音符速度 0~1
let velocity = event.velocity
// 其他信息可以打印 event 查看
console.log(event)
})
// 监听其他事件
input.addListener("noteoff", () => {})
这样我们就完成了宿主软件和浏览器的绑定。
3.事件分发
以 React 为例,我们需要实现简单的订阅模式,以便分发MIDI事件到各组件,这里采用 mitt.js。
简单来说我们需要实现两个 Hook,一个用于监听的 useMidiListener(),一个用于发送的 useMidiTrigger(),架构图如下:

代码如下:
import {useEffect} from "react";
import mitt from 'mitt';
import {type InputEventMap} from "webmidi";
// 创建事件总线
const bus = mitt();
// 事件类型
export type MidiEventName = keyof InputEventMap
// 用于发送事件,传入事件类型
export function useMidiTrigger(name: MidiEventName) {
return (event: any) => {
bus.emit(name, event);
}
}
// 用于监听 指定通道 & 指定类型 的事件
export function useMidiListener( channel: number, name: MidiEventName, callback: (event: any) => void ) {
// 按通道过滤事件
let handler = (event: any) => event.message.channel === channel && callback(event)
useEffect(() => {
bus.on(name, handler);
return () => bus.off(name, handler);
}, []);
}
如何使用?
我们先创建触发器,并且将它绑定到输入端口的监听事件上:
// 创建触发器,关联音符按下事件
const note_on_trigger = useMidiTrigger("noteon")
useEffect(() => {
// 绑定触发器到音符按下事件
input?.addListener("noteon", note_on_trigger)
return () => {
// 销毁时解绑
input?.removeListener("noteon", note_on_trigger)
}
}, []);
然后可以在任意地方创建监听器,就能收到回调。
// 当通道 16 有音符按下,触发回调
useMidiListener(16, "noteon", (event) => {
// 这里可以执行动画等...
console.log(event)
})
现在我们就能愉快的将 MIDI 事件转为可视化了。
4.MIDI 事件详解
| 事件 | 长度 | 值 | 说明 |
|---|---|---|---|
| noteon | 3 | [状态字节,音高, velocity] | 音符按下 |
| noteoff | 2 | [音高, release] | 音符抬起 |
| controlchange | 2 | [CC通道号, 事件值] | CC事件 |
| start | 0 | / | 开始播放 |
| songposition | 2 | [拍数, 拍数进位] | 歌曲位置,每小格 |
| continue | 0 | / | 继续播放 |
镜头运动约定
通过音符等控制摄像机运动,约定以下协议。
附:CC 控制器用法约定
| CC# | 描述(Purpose) | 值范围 | 典型用途/说明 |
|---|---|---|---|
| 0 | Bank Select (MSB) | 0-127 | 银行选择(高位),与 CC32 和 Program Change 结合切换补丁。 |
| 1 | Modulation Wheel (MSB) | 0-127 | 调制轮(高位),通常控制颤音或效果深度。 |
| 2 | Breath Controller (MSB) | 0-127 | 呼吸控制器(高位),用于动态调制。 |
| 3 | Undefined | 0-127 | 未定义,可自定义。 |
| 4 | Foot Controller (MSB) | 0-127 | 脚踏控制器(高位),常用于音量或效果。 |
| 5 | Portamento Time (MSB) | 0-127 | 滑音时间(高位),控制音高滑移速度。 |
| 6 | Data Entry (MSB) | 0-127 | 数据输入(高位),用于 RPN/NRPN 参数。 |
| 7 | Main Volume (MSB) | 0-127 | 主音量(高位),通道整体音量控制。 |
| 8 | Balance (MSB) | 0-127 | 平衡(高位),左右声道平衡(64 为中心)。 |
| 9 | Undefined | 0-127 | 未定义。 |
| 10 | Pan (MSB) | 0-127 | 平移(高位),左右声像定位(64 为中心)。 |
| 11 | Expression (MSB) | 0-127 | 表情(高位),动态音量变化(相对于 CC7)。 |
| 12 | Effect Control 1 (MSB) | 0-127 | 效果控制 1(高位),如混响深度。 |
| 13 | Effect Control 2 (MSB) | 0-127 | 效果控制 2(高位),如延迟。 |
| 14-15 | Undefined | 0-127 | 未定义。 |
| 16-19 | General Purpose Controllers (MSB) | 0-127 | 通用控制器(高位),如滑块。 |
| 20-31 | Undefined | 0-127 | 未定义,可自定义。 |
| 32 | Bank Select (LSB) | 0-127 | 银行选择(低位),与 CC0 结合。 |
| 33 | Modulation Wheel (LSB) | 0-127 | 调制轮(低位)。 |
| 34 | Breath Controller (LSB) | 0-127 | 呼吸控制器(低位)。 |
| 36 | Foot Controller (LSB) | 0-127 | 脚踏控制器(低位)。 |
| 37 | Portamento Time (LSB) | 0-127 | 滑音时间(低位)。 |
| 38 | Data Entry (LSB) | 0-127 | 数据输入(低位)。 |
| 39 | Channel Volume (LSB) | 0-127 | 通道音量(低位)。 |
| 40 | Balance (LSB) | 0-127 | 平衡(低位)。 |
| 42 | Pan (LSB) | 0-127 | 平移(低位)。 |
| 43 | Expression (LSB) | 0-127 | 表情(低位)。 |
| 44 | Effect Control 1 (LSB) | 0-127 | 效果控制 1(低位)。 |
| 45 | Effect Control 2 (LSB) | 0-127 | 效果控制 2(低位)。 |
| 46-63 | LSB for 14-31 | 0-127 | 对应 14-31 的低位,未广泛使用。 |
| 64 | Hold Pedal (Sustain) | ≤63 off, ≥64 on | 延音踏板(开关),保持音符持续。 |
| 65 | Portamento On/Off | ≤63 off, ≥64 on | 滑音开关。 |
| 66 | Sostenuto Pedal | ≤63 off, ≥64 on | 持续踏板(仅保持按下时音符)。 |
| 67 | Soft Pedal | ≤63 off, ≥64 on | 柔音踏板,降低音量。 |
| 68 | Legato Pedal | ≤63 off, ≥64 on | 连奏踏板。 |
| 69 | Hold 2 (Pedal) | ≤63 off, ≥64 on | 保持 2(渐消)。 |
| 70 | Sound Variation | 0-127 | 声音变化(如音色)。 |
| 71 | Timbre/Harmonic Content | 0-127 | 音色/谐波内容(谐振)。 |
| 72 | Release Time | 0-127 | 释放时间。 |
| 73 | Attack Time | 0-127 | 起音时间。 |
| 74 | Brightness (Cutoff Frequency) | 0-127 | 亮度(滤波截止频率)。 |
| 75-79 | Sound Controllers 6-10 | 0-127 | 声音控制器,制造商自定义。 |
| 80 | General Purpose Button 1 (Decay) | ≤63 off, ≥64 on | 通用按钮 1(衰减)。 |
| 81 | General Purpose Button 2 | ≤63 off, ≥64 on | 通用按钮 2(高通滤波)。 |
| 82-83 | General Purpose Buttons 3-4 | ≤63 off, ≥64 on | 通用按钮 3-4。 |
| 84 | Portamento Control | 0-127 | 滑音控制。 |
| 85-87 | Undefined | — | 未定义。 |
| 88 | High-Resolution Velocity Prefix | 0-127 | 高分辨率力度前缀。 |
| 89-90 | Undefined | — | 未定义。 |
| 91 | Reverb Send Level | 0-127 | 混响深度。 |
| 92 | Tremolo Level | 0-127 | 颤音深度。 |
| 93 | Chorus Level | 0-127 | 合唱深度。 |
| 94 | Detune/Celeste Level | 0-127 | 失谐/天籁深度。 |
| 95 | Phaser Level | 0-127 | 相位器深度。 |
| 96 | Data Increment | N/A | 数据递增(用于 RPN/NRPN)。 |
| 97 | Data Decrement | N/A | 数据递减。 |
| 98 | NRPN LSB | 0-127 | 非注册参数号(低位)。 |
| 99 | NRPN MSB | 0-127 | 非注册参数号(高位)。 |
| 100 | RPN LSB | 0-127 | 注册参数号(低位)。 |
| 101 | RPN MSB | 0-127 | 注册参数号(高位)。 |
| 102-119 | Undefined | — | 未定义。 |
| 120 | All Sound Off | 0 | 所有声音关闭(立即静音)。 |
| 121 | Reset All Controllers | 0 | 重置所有控制器。 |
| 122 | Local Control | 0 off, 127 on | 本地控制开关。 |
| 123 | All Notes Off | 0 | 所有音符关闭。 |
| 124 | Omni Mode Off | 0 | 全向模式关闭。 |
| 125 | Omni Mode On | 0 | 全向模式开启。 |
| 126 | Mono Mode On | 0-16 | 单声道模式(值=通道数)。 |
| 127 | Poly Mode On | 0 | 多声道模式。 |