译注 :本文是作者 Peter Anglea 发布在 《SmashingMagazine》上的一篇介绍 Web MIDI API 及用其开发一款电子钢琴游戏的文章。 (转载请注明出处)原文链接:www.smashingmagazine.com/2018/03/web…
原文作者: Peter Anglea 译者: 西楼听雨
随着 Web 的不断发展,浏览器新技术的不断涌现,本地开发和 Web 开发之间的分界线变得越来越模糊。新的 API 使得在浏览器中开发各类新型软件的能力得到释放。
就在不久之前,与数码乐器进行交互的能力还一直被局限在本地和桌面应用中,现在,Web MIDI API 的到来就是为了改变这个现状。
在本文中,我们会对 MIDI 及 Web MIDI API 进行基本的讨论,展示它对于创建一个可以响应乐器相关输入的Web应用有多简单。
什么是 MIDI?
MIDI 其实已经存在相当长的一段时间了,但在浏览器中的亮相还是首次。MIDI (Musical Instrument Digital Interface : 乐器数字接口)是一种技术标准,于1983年首次发布,旨在为数码乐器,混音合成器,电脑,及各类音频设备之间的通讯创建一种方式。在这些设备之间传递的 MIDI 消息是音符的和基于时间的信息。
一套典型的 MIDI 配置,通常会有一个数字钢琴键盘,这个键盘可以把各类消息,如音调、颤音、音量、平移、调节等等发送给一个混音合成器,进而转化为可听见的声音,也可以向桌面类音频音符化展示软件及数字音频工作站(DAW)发送信号,进而转化为音符,保存为文件等。
MIDI 是一个非常多才的协议。除了播放和录制音乐外,它已经成为舞台、剧院类应用的一种标准协议,常用于灯光设备的控制及场景信息的提示。
浏览器中的 MIDI
WebMIDI API 通过 javascript 给浏览器带来了所有 MIDI 所具备的好处。我们只需要学习几个方法和对象即可。
介绍
首先,这有一个 navigator.requestMIDIAccess()
方法,它的作用就和它名字一样——发起访问连接到你电脑上的 MIDI 设备的请求。你可以通过检查这个方法的存在性来确认浏览器是否支持这个 API。
if (navigator.requestMIDIAccess) {
console.log('该浏览器支持 WebMIDI!');
} else {
console.log('WebMIDI 不被该浏览器支持。');
}
复制代码
第二,我们还有一个 MIDIAccess 对象,它包含了所有可用的输入设备(如钢琴键盘)、输出设备(如混音合成器)的引用。调用 requestMIDIAccess() 方法会返回一个 promise,如果浏览器与你的 MIDI 设备连接成功,它将返回一个 MIDIAccess 对象并将其作为连接成功的回调函数的一个参数。
navigator.requestMIDIAccess()
.then(onMIDISuccess, onMIDIFailure);
function onMIDISuccess(midiAccess) {
console.log(midiAccess);
var inputs = midiAccess.inputs;
var outputs = midiAccess.outputs;
}
function onMIDIFailure() {
console.log('无法访问你的 MIDI 设备。');
}复制代码
第三,MIDI 消息在输入和输出设备之前的往返都是通过一个 MIDIMessageEvent
进行传递的。这些消息包含了关于 MIDI 事件的信息,如“音调”、“音频”、“力度”、“时间”等等。我们可以通过向这些输入、输出设备添加回调函数(监听器)来接收这些消息。
深入“腹地”
我们接着深入。为了让 MIDI 设备可以发送消息到浏览器中,我们需要在每个 input 上添加一个 onmidimessage
监听器,每次当输入设备发送一个消息,例如钢琴键盘的一次按键,这个回调就会被触发。
我们可以像下面这样遍历 inputs 添加监听器:
function onMIDISuccess(midiAccess) {
for (var input of midiAccess.inputs.values())
input.onmidimessage = getMIDIMessage;
}
}
function getMIDIMessage(midiMessage) {
console.log(midiMessage);
}复制代码
我们取到的 MIDIMessageEvent
对象会包含许多信息,但我们最感兴趣的是它的数据数组(data 属性)。通常这个数组包含三个值(例如:[144, 72, 64]
),第一个值告诉我们发送的命令是什么类型,第二个是 note 值(译:可理解为音符、键位值),第三个是力度值(velocity)。命令的类型可以是“note on”、“note off”、控制器(如弯音轮、钢琴踏板)及其他与这台设备相关的系统专用的事件。
考虑到本文的主旨,我们的焦点只集中在识别“note no”和“note off”消息上。下面是他们的基本概念:
- 命令类型值为 144 时,表示“note on”事件,128 则表示“note off”事件。
- note 值的范围为 0-127。例如,在88键钢琴上,最小的值为 21,最大的值为 108。“C 中键”的值为 60。
- 力度值的范围也是 0-127 (最温和到“最喧闹”)。事实上可能的最温和的“note on”时的力度值为 1。
- 有时会将 144 命令类型值伴随着 0 力度值来表示“note off”消息,所以对于 0 力度值得检查也是识别“note off”所必要的。
基于以上认识,我们可以像下面这样展开前面我们的 getMIDIMessage
示例 :通过对来自输入设备的 MIDI 消息的分析,将不同意义上的消息进一步传递给其他处理函数进行处理。
function getMIDIMessage(message) {
var command = message.data[0];
var note = message.data[1];
var velocity = (message.data.length > 2) ? message.data[2] : 0; // 在 noteoff 命令中,不一定会包含 velocity 值
switch (command) {
case 144: // noteOn
if (velocity > 0) {
noteOn(note, velocity);
} else {
noteOff(note);
}
break;
case 128: // noteOff
noteOff(note);
break;
// we could easily expand this switch statement to cover other types of commands such as controllers or sysex
// 我们也以可非常容易地扩展这个 switch 语句来覆盖其他类型的命令
}
}复制代码
浏览器兼容性及相关垫片库(polyfill)
在写作本文的这个时间点,Web MIDI API 仅被 Chrome、Opera、安卓 WebView 从本地上支持。
对于其他不支持的浏览器,Chris Wilson 的 WebMIDIAPIShim 库可以作为 Web MIDI API 的一个垫片库使用。只需要在你的页面上引用这个垫片脚本,就可以拥有上面提到的所有特性。
<script src="WebMIDIAPI.min.js"></script>
<script>
if (navigator.requestMIDIAccess) { //... returns true
</script>复制代码
不过,要使用这个垫片库,还需要安装 Jazz-Soft.net 的 Jazz-Plugin,也就是说,非常不幸,虽然对于只是需要保持灵活性的开发人员来说是没关系的,但是对于主流人群的话就是一个障碍了。但愿,随着时间的发展,其他浏览器也会相继对其进行本地上的支持。
使用 webmidi.js 来使我们的工作变的简单
目前为止,对于 WebMIDI API 带来的所有可能性,我们还只是做了比较肤浅、局部的了解。如果还要支持除了基础的“note on”和“note off”消息外的其他功能,事情就会变得非常复杂。
如果你想找到一个不错的 javascript 库来急剧地简化你的代码,请选择由 Jean-Philippe Côté 发布在 Github 上的 WebMidi.js。这个库对 MIDIAccess
和 MIDIMessageEvent
的解析做了一个很好的抽象,让你可以以一种极简单的方式添加、移除某个特定事件的监听器。
WebMidi.enable(function () {
// 查看可用的输入、输出设备
console.log(WebMidi.inputs);
console.log(WebMidi.outputs);
// 通过 name、id 或者 index 来获取一个输入设备
var input = WebMidi.getInputByName("My Awesome Keyboard");
// 或者
// input = WebMidi.getInputById("1809568182");
// input = WebMidi.inputs[0];
// 在所有通道监听 'note on' 消息
input.addListener('noteon', 'all',
function (e) {
console.log("收到 'noteon' 消息 (" + e.note.name + e.note.octave + ").");
}
);
// 在通道3监听 'pitchben' 消息
input.addListener('pitchbend', 3,
function (e) {
console.log("收到 'pitchbend' 消息.", e);
}
);
// 在所有通道监听“controlchange”消息
input.addListener('controlchange', "all",
function (e) {
console.log("收到'controlchange' 消息.", e);
}
);
// 移除所有通道上的 'noteoff' 监听器
input.removeListener('noteoff');
// 移除所有监听器
input.removeListener();
});
复制代码
真实场景:制作一个由钢琴键盘控制的室内脱逃游戏
几个月前,我和我的妻子做了个决定,决定在家里创建一个“室内逃脱”体验的决定,以给我们的朋友和家庭带来欢乐。我们想着这个游戏应该包含一些特效以提升体验。但不幸,我俩都没有过硬的工程技能,利用磁铁、激光以及电线制作复杂的锁具和特效都在我俩的专业范围之外。不过,我会——我对我的和浏览器打交道的工作非常了解,恰好我们又有一台电子钢琴。
因此,这个想法就随之浮现了。我们决定把制作在电脑上的一系列密码锁作为游戏的核心部分,玩家需要在我们的钢琴上弹出指定的音符序列才能解锁,a la Willy Wonka.
听起来很酷是吧?那我们来看下如何实现它吧。
起架
首先我们将从发起访问 WebMIDI 请求开始,然后对我们的键盘进行识别,接着添加相应的事件监听器,同时创建几个变量和函数来给我们在游戏的各个阶段提供帮助。
// 该变量告诉我们游戏当前所在的步骤。
// 我们将在之后解析 noteOn/Off 消息时使用它
var currentStep = 0;
// 请求访问 MIDI
if (navigator.requestMIDIAccess) {
console.log('该浏览器支持 Web MIDI!');
navigator.requestMIDIAccess().then(onMIDISuccess, onMIDIFailure);
} else {
console.log('WebMIDI 不被该浏览器所支持.');
}
// 该函数用于在 requestMIDIAccess 成功后执行
function onMIDISuccess(midiAccess) {
var inputs = midiAccess.inputs;
var outputs = midiAccess.outputs;
// 对每个 input 添加 MIDI 事件监听器
for (var input of midiAccess.inputs.values()) {
input.onmidimessage = getMIDIMessage;
}
}
// 该函数用户在 requestMIDIAccess 失败后执行
function onMIDIFailure() {
console.log('错误: 无法访问 MIDI 设备.');
}
// 该函数用于对我们接收到的 MIDI 消息进行解析
// 在这个应用中,我们只关心 note 值
// 当然我们也可以对其他信息进行解析
function getMIDIMessage(message) {
var command = message.data[0];
var note = message.data[1];
var velocity = (message.data.length > 2) ? message.data[2] : 0;
switch (command) {
case 144: // note on
if (velocity > 0) {
noteOn(note);
} else {
noteOff(note);
}
break;
case 128: // note off
noteOffCallback(note);
break;
}
}
// 该函数用于处理 noteOn 消息(即,琴键被按下)
// 可以把他想象成 'onkeydown' 事件
function noteOn(note) {
//...
}
// 该函数用于处理 noteOff 消息(即,琴键被释放)
// 可以把他想象成 'onkeyup' 事件
function noteOff(note) {
//...
}
// 该函数用于触发特定的动画和推动游戏进入下一个环节
// 例如,一把锁被解开之后,或者计时器完成之后
function runSequence(sequence) {
//...
}
复制代码
第1步:按任意键开始
要开始游戏,玩家只需按任意键即可。这个步骤非常容易,同时可以向他们展示一些游戏玩法的信息,接着启动一个倒计时计时器。
function noteOn(note) {
switch(currentStep) {
// 如有游戏还没有开始,
// 那么我们收到的第一个 noteOn 消息将触发第一个序列的运行
case 0:
// 运行我们的游戏开始序列
runSequence('gamestart');
// 增加 currentStep,以确保该序列只被执行一次
currentStep++;
break;
}
}
function runSequence(sequence) {
switch(sequence) {
case 'gamestart':
// 现在开始启动倒数计时器
startTimer();
// 触发动画的及给与第一把锁的线索的代码
break;
}
}
复制代码
第2步:弹奏出正确的音符序列
第一把锁要求玩家必须按照正确的顺序弹奏出一个特定的符号序列。
要实现这个锁,对于每个“note on”消息,我们需要把 note 值附加到一个数组后面,然后检查它是否与一个预定义的数组匹配。
我们假设玩家可以根据我们在室内的声音提示找到对应的键位,在本例中,我们以F大调的歌曲"Amazing Grace"的开头为提示音。它的键位序列如下图所示。
var correctNoteSequence = [60, 65, 69, 65, 69, 67, 65, 62, 60]; // Amazing Grace in F
var activeNoteSequence = [];
function noteOn(note) {
switch(currentStep) {
// ... (case 0)
// 第一把锁——弹奏出正确的序列
case 1:
activeNoteSequence.push(note);
// 当数组的长度与正确的数组的一样是,进行匹配
if (activeNoteSequence.length == correctNoteSequence.length) {
var match = true;
for (var index = 0; index < activeNoteSequence.length; index++) {
if (activeNoteSequence[index] != correctNoteSequence[index]) {
match = false;
break;
}
}
if (match) {
// 运行下一个序列,并增加当前步骤值
runSequence('lock1');
currentStep++;
} else {
// 清空数组,以重头计算
activeNoteSequence = [];
}
}
break;
}
}
function runSequence(sequence) {
switch(sequence) {
// ...
case 'lock1':
// 触发动画并给与下一把锁的提示的代码
break;
}
}
复制代码
弟3步:弹奏出正确的和弦(译:同时按下键位的组合)
下一把锁要求玩家找到正确的键位组合(同时),这个时候就是“note off”登台的时候了。对于每个“note on”消息,我们会把其 note 值添加到一个数组,而对于每个“note off”消息,我们又会把它的 note 值从数组中移除,这样的话这个数组就可以反应出任意时刻的键位组合状态。然后,剩下的就是在每次添加一个 note 值时对这个数组进行验证了,看看它是否匹配我们的目标数组。
我们将正确答案设置为由中 C 键开始的 C7 和弦,像下图所示的这样。
var correctChord = [60, 64, 67, 70]; // C7 chord starting on middle C
var activeChord = [];
function noteOn(note) {
switch(currentStep) {
// ... (case 0, 1)
case 2:
// 把该 note 值添加至实时数组
activeChord.push(note);
// 如果数组的长度与正确答案的一致,则进行匹配
if (activeChord.length == correctChord.length) {
var match = true;
for (var index = 0; index < activeChord.length; index++) {
if (correctChord.indexOf(activeChord[index]) < 0) {
match = false;
break;
}
}
if (match) {
runSequence('lock2');
currentStep++;
}
}
break;
}
function noteOff(note) {
switch(currentStep) {
case 2:
// 从实时数组中移除该 note 值
activeChord.splice(activeChord.indexOf(note), 1);
break;
}
}
function runSequence(sequence) {
switch(sequence) {
// ...
case 'lock2':
// 触发动画,停止计时器,并结束游戏
stopTimer();
break;
}
}
复制代码
到这,剩下的就是添加一些额外的界面元素和动画了,然后我们的游戏就可以开始使用了。
下面是一个该游戏从开始到结束的完整操作的视频,是在 Google Chrome 下演示的,同时也会显示一个虚拟的 MIDI 键盘以帮助查看当前各键位的按压状态。正常来说,我们应该把这种室内逃脱场景的游戏以全屏模式运行,并拿掉其他的输入设备(如鼠标、电脑键盘),以此防止用户关闭游戏窗口。
游戏演示视频: v.youku.com/v_show/id_X…
如果你身边没有 MIDI 设备,而你又想做一下尝试,没关系,网上有许多虚拟 MIDI 键盘类应用可以把你的电脑键盘作为乐器,如,VMPK。如果你想对上面这个游戏做详细探究,可 check out 这个游戏在 CodePen 上的完整原型。
该游戏的 CodePen 链接: codepen.io/peteranglea…
结语
MIDI.org 上有一句话“在相当常的一段时间内,Web MIDI 有可能会是最具颠覆性的音频类技术,也许会跟 MIDI 在1983年时最初的那样。”这是一个非常高的要求也是一个极高的赞美。
对于新型的、令人激动的基于浏览器的音乐应用的发展,这个 API 所带来的推动效果,我希望本文及文中的示例应用已经使你感受到了。很有可能,在接下来的几年,我们可以开始看到更多的在线音符类软件,如数字音频工作站,音频可视化应用,乐器教程等等。
如果你希望了解更多关于 Web MIDI 以及它所具备的能力的信息,我推荐你阅读下面这些:
- “让Web摇滚起来(Making the Web Rock: Web MIDI),” Chris Wilson, Web MIDI 标准的合编者. (幻灯片版)
- “Web MIDI 介绍(Introduction to Web MIDI),” Stuart Memo, Envato Tuts+
- “创建通过 MIDI 控制的基于浏览器的音频应用(Creating Browser-Based Audio Applications Controlled by MIDI Hardware),” Stéphane P. Péricat, Toptal
如果想获得更多的启发,下面是其他一些经过实践的案例:
- Web 音频 MIDI 合成器(Web Audio MIDI Synthesizer)
一个可以通过 MIDI 设备控制的简单的合成器 - Web 音频电子鼓乐合成器(Web Audio Drum Machine)
一个有趣的可以制作你自己的鼓循环的应用(A fun app to create your own drum loops) - Noteflight
一个在线乐谱制作应用,支持通过 Web MIDI 作为可能的输入方法