浏览器中 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 事件详解

事件长度说明
noteon3[状态字节,音高, velocity]音符按下
noteoff2[音高, release]音符抬起
controlchange2[CC通道号, 事件值]CC事件
start0/开始播放
songposition2[拍数, 拍数进位]歌曲位置,每小格
continue0/继续播放

镜头运动约定

通过音符等控制摄像机运动,约定以下协议。

附:CC 控制器用法约定

CC#描述(Purpose)值范围典型用途/说明
0Bank Select (MSB)0-127银行选择(高位),与 CC32 和 Program Change 结合切换补丁。
1Modulation Wheel (MSB)0-127调制轮(高位),通常控制颤音或效果深度。
2Breath Controller (MSB)0-127呼吸控制器(高位),用于动态调制。
3Undefined0-127未定义,可自定义。
4Foot Controller (MSB)0-127脚踏控制器(高位),常用于音量或效果。
5Portamento Time (MSB)0-127滑音时间(高位),控制音高滑移速度。
6Data Entry (MSB)0-127数据输入(高位),用于 RPN/NRPN 参数。
7Main Volume (MSB)0-127主音量(高位),通道整体音量控制。
8Balance (MSB)0-127平衡(高位),左右声道平衡(64 为中心)。
9Undefined0-127未定义。
10Pan (MSB)0-127平移(高位),左右声像定位(64 为中心)。
11Expression (MSB)0-127表情(高位),动态音量变化(相对于 CC7)。
12Effect Control 1 (MSB)0-127效果控制 1(高位),如混响深度。
13Effect Control 2 (MSB)0-127效果控制 2(高位),如延迟。
14-15Undefined0-127未定义。
16-19General Purpose Controllers (MSB)0-127通用控制器(高位),如滑块。
20-31Undefined0-127未定义,可自定义。
32Bank Select (LSB)0-127银行选择(低位),与 CC0 结合。
33Modulation Wheel (LSB)0-127调制轮(低位)。
34Breath Controller (LSB)0-127呼吸控制器(低位)。
36Foot Controller (LSB)0-127脚踏控制器(低位)。
37Portamento Time (LSB)0-127滑音时间(低位)。
38Data Entry (LSB)0-127数据输入(低位)。
39Channel Volume (LSB)0-127通道音量(低位)。
40Balance (LSB)0-127平衡(低位)。
42Pan (LSB)0-127平移(低位)。
43Expression (LSB)0-127表情(低位)。
44Effect Control 1 (LSB)0-127效果控制 1(低位)。
45Effect Control 2 (LSB)0-127效果控制 2(低位)。
46-63LSB for 14-310-127对应 14-31 的低位,未广泛使用。
64Hold Pedal (Sustain)≤63 off, ≥64 on延音踏板(开关),保持音符持续。
65Portamento On/Off≤63 off, ≥64 on滑音开关。
66Sostenuto Pedal≤63 off, ≥64 on持续踏板(仅保持按下时音符)。
67Soft Pedal≤63 off, ≥64 on柔音踏板,降低音量。
68Legato Pedal≤63 off, ≥64 on连奏踏板。
69Hold 2 (Pedal)≤63 off, ≥64 on保持 2(渐消)。
70Sound Variation0-127声音变化(如音色)。
71Timbre/Harmonic Content0-127音色/谐波内容(谐振)。
72Release Time0-127释放时间。
73Attack Time0-127起音时间。
74Brightness (Cutoff Frequency)0-127亮度(滤波截止频率)。
75-79Sound Controllers 6-100-127声音控制器,制造商自定义。
80General Purpose Button 1 (Decay)≤63 off, ≥64 on通用按钮 1(衰减)。
81General Purpose Button 2≤63 off, ≥64 on通用按钮 2(高通滤波)。
82-83General Purpose Buttons 3-4≤63 off, ≥64 on通用按钮 3-4。
84Portamento Control0-127滑音控制。
85-87Undefined未定义。
88High-Resolution Velocity Prefix0-127高分辨率力度前缀。
89-90Undefined未定义。
91Reverb Send Level0-127混响深度。
92Tremolo Level0-127颤音深度。
93Chorus Level0-127合唱深度。
94Detune/Celeste Level0-127失谐/天籁深度。
95Phaser Level0-127相位器深度。
96Data IncrementN/A数据递增(用于 RPN/NRPN)。
97Data DecrementN/A数据递减。
98NRPN LSB0-127非注册参数号(低位)。
99NRPN MSB0-127非注册参数号(高位)。
100RPN LSB0-127注册参数号(低位)。
101RPN MSB0-127注册参数号(高位)。
102-119Undefined未定义。
120All Sound Off0所有声音关闭(立即静音)。
121Reset All Controllers0重置所有控制器。
122Local Control0 off, 127 on本地控制开关。
123All Notes Off0所有音符关闭。
124Omni Mode Off0全向模式关闭。
125Omni Mode On0全向模式开启。
126Mono Mode On0-16单声道模式(值=通道数)。
127Poly Mode On0多声道模式。