Web MIDI 入門:如何用電子鋼琴做一款遊戲

西樓聽雨發表於2018-03-24
譯註:本文是作者 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 是一個非常多才的協議。除了播放和錄製音樂外,它已經成為舞臺、劇院類應用的一種標準協議,常用於燈光裝置的控制及場景資訊的提示。

A performer plays a digital piano onstage


瀏覽器中的 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 從本地上支援。

Web MIDI 入門:如何用電子鋼琴做一款遊戲

對於其他不支援的瀏覽器,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.

This is a musical lock
這是一個音樂鎖

聽起來很酷是吧?那我們來看下如何實現它吧。


起架

首先我們將從發起訪問 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"的開頭為提示音。它的鍵位序列如下圖所示。

A visual representation of the first nine notes of “Amazing Grace” on a piano
它正確的 MIDI 音符陣列為: [60, 65, 69, 65, 69, 67, 65, 62, 60].
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 和絃,像下圖所示的這樣。

A visual representation of a C7 chord on a piano

它的正確的 MIDI note 值陣列為: [60, 64, 67, 70].

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 音訊 MIDI 合成器(Web Audio MIDI Synthesizer
    一個可以通過 MIDI 裝置控制的簡單的合成器
  • Web 音訊電子鼓樂合成器(Web Audio Drum Machine
    一個有趣的可以製作你自己的鼓迴圈的應用(A fun app to create your own drum loops)
  • Noteflight
    一個線上樂譜製作應用,支援通過 Web MIDI 作為可能的輸入方法


相關文章