天天看點

【Win 10 應用開發】MIDI 音樂合成——音符消息篇

在上一篇中,老周介紹了一些樂理知識,有了那些常識後,進行 MIDI 程式設計就簡單得多了。盡管微軟已經把 API 封裝好,用起來也很簡單,但是,如果你沒有相應的音樂知識基礎,你是無法進行 MIDI 程式設計的。

這一篇老周将給你講述一下如何讓你的聲霸卡播放一個音符,這會包含兩條消息,而且這兩條消息是很常用的。

1、Note On:讓 MIDI 裝置(如果沒有專業裝置,那就是你的聲霸卡)發出某個音符的聲音,比如,發出中音 3 的聲音。注意啊,Note on 一旦發送,裝置會一直播放這個聲音,要想停止播放一個音符,你就要用到下面這條消息,它們是天生的一對。

2、Note Off:關閉某個音符,即停止播放某個音符。

咱們先來了解三個很重要的類,跟 MIDI 裝置通信相關的 API 都在 Windows.Devices.Midi 命名空間下,封裝好的。

1、MidiInPort:用來從 MIDI 輸入裝置接收消息,是以它公開了一個 MessageReceived 事件,隻要 MIDI 輸入裝置發送了消息,就會引發這個事件,這時候你可以處理這個事件,把收到的消息再傳到聲霸卡上進行播放。MIDI 輸入裝置一般是 MIDI 鍵盤,估計大部分人用不上這個類,因為一般人不會購買 MIDI 鍵盤。真想買個好用的,起碼是 88 鍵的,價格還是不低的。

2、MidiOutPort:連接配接 MIDI 輸出裝置,可以播放 MIDI 音樂。如果沒有專業的 MIDI 音響,就可以連到你的聲霸卡上,内置外置都可以,市面上有外置的 MIDI 聲霸卡賣,當然了,想省錢的話,你是買不到好音色的,要是你不在乎音色的話,那無所謂。

3、MidiSynthesizer:這個類非常好使,它其實類似于 MidiOutPort 類,但它可以自動選擇預設的裝置(當然也可選擇裝置)。這個類是專門針對 MIDI 合成而設計的,盡管它與 MidiOutPort 相似,但側重點不同。MidiOutPort 側重于與 MIDI 裝置的通信,而 MidiSynthesizer 類是側重于合成。

我們在進行電子音樂合成的時候,隻需要使用 MidiSynthesizer 類即可,它沒有構造函數,可以調用 CreateAsync 靜态方法來擷取執行個體。對于普通裝置而言,我們調用無參數的重載版本就行了,應用程式會預設選擇聲霸卡作為輸出裝置。然後,我們盡管發送 MIDI 消息就OK。當不再使用 MidiSynthesizer 執行個體時,應該把它 Dispose 掉,以釋放資源占用。

是不是很簡單呢,一切都是封裝好的,是以說,你隻要有一定的樂理基礎就可以輕松玩耍這些 API。據說,這個 MidiSynthesizer 類還包含了羅蘭公司(Roland)的通用音色庫。

當然了,這隻能是通用的 128 種樂器的聲音,不包含各種演奏技巧(如揉弦、波音、顫音等)。其目的是盡可能地相容各類聲霸卡,包括很爛的聲霸卡,雖然比較普通,不過嘛,音色聽着還是可以的,隻是少了點感覺。不過也是,電聲畢竟是虛假的樂音,而不是自然音,就算是專業級别的音源,其實聽着也不會太有樂感的。是以嘛,真想感受音樂之美,還是買個真實的樂器自己去演奏。老周小時候喜歡口琴和笛子,上國中的時候,學了一點電子琴、口風琴和揚琴,不過隻是學了一點點而已。上高中的三年基本沒碰過樂器。大學的時候,在學生會裡面鬼混,是以經常可以拿樂隊的吉他撥兩下。

後來,像洞箫、巴烏、葫蘆絲、陶埙、陶笛等都學過。想學學古琴,但是買一把好琴比較貴,就沒有去學了。吹奏類樂器一般比較便宜,至少像老周這種窮人還能買得起,是以老周家裡放的樂器,多數是吹奏類的。擊打類的有一對小銅鼓,在路邊撿的。

好,不扯了,咱們說正題。本篇的重點是學會兩條 MIDI 消息,對,就是上面說的 Note on 和 Note off。不管是 on 還是 off,這兩條音符消息的格式是一樣的,都是包含三個位元組。

第一個位元組是 【狀态碼 + 通道編号】,這個可能你不太了解,沒事,老周待會兒再解釋。

第二個位元組是音符,對,就是上一篇中,簡譜上面的 1234567,唱出來就是 dol re mi fa sol la xi,用一個位元組表示,從 0 - 127,共128 個音符。

第三個位元組是音速,值也是從 0 到 127。這個音速其實你感覺不到什麼,發送到聲霸卡上的效果就是音量。值越小聲音越小,如果是 0 就等于靜音了,127 時聲音最大。

好,下面逐個解釋兩下。

首先,狀态碼,在前一篇中,老周簡單地說了一下 MIDI 檔案的結構,一個 MIDI 事件是由 delta-time 和事件主體組成。而一個事件的開頭都有一個标志位元組。在MIDI檔案中, Note on 和 Note off 都是一個事件;而在實時通信中,可認為是一條 MIDI 消息,其實結構是一樣的。

不管是Note on 和 Note off ,還是其他通道消息,其第一個位元組是由兩部分資訊組成的。我們知道,一個位元組有 8 位,從右邊起,1 - 4位表示通道編号,是以,MIDI 音樂有 16 個通道。為什麼是 16 個通道呢,不是剛說了嗎,隻有 4 位二進制位表示通道編号,二進制 1111 就是 15,是以,通道的有效編号是 0 - 15,共16個。

注意:軌道與通道不同。軌道地用于 MIDI 檔案的,可以是單軌,可以是多軌,軌道隻是友善存儲,也友善人類檢視,但 MIDI 設定并不認軌道,隻認識标準的 16 個通道。故 MIDI 消息隻有通道的概念。另外,還要注意,第 10 個通道(編号 9 )是打擊樂專用通道,在 GM 2 标準中,增加了一個,即第 10、11 通道可用于打擊樂(編号 9、10)。

第 5 到 8 位表示狀态碼,或者說事件标志,總之,用來辨別某個指令。Note Off 的标志是 1000,換算為十六進制就是 0x8 ;Note On 的标志是 1001,換算為十六進制就是 0x9。

假設,要向第四個通道發送一條 Note on 消息。第四個通道的編号是 3,換算為二進制就是 0011,Note on 的标志為 1001,是以,組合起來,第一個位元組就是 1001 0011,換算為十六進制就是 0x93。再比如,要向第一個通道發送一條消息,第一通道的編号是0,即 0000,Note on 的标志是 1001,組合起來的位元組就是 1001 0000,換算為十六進制就是 0x90。

如果要向第二個通道發送一條 Note off 消息。第二個通道的編号是 1,即 0001,Note off 的标志為 1000,組合起來的位元組就是 0x81。

音符消息的第二個位元組是音符,值從 0 - 127,共128個。雖然有 128 個音符,但實際上你隻要記住一個值就行了—— 60,它表示的是中音 1 。128 / 12,餘數為 8 ,湊不成一個 12,是以,中音 1 就位于 120 / 2 = 60 處。為什麼音符是 12 個一組呢?上一篇中老周為啥要介紹“十二平均律”,就是有用的,MIDI 的音符排序是遵守十二平均律的,是以每 12 個音符構成一個“八度”。

于是這一來,這裡頭就有十來個八度了,其實我們大多數歌曲根本用不上,很多情況下,隻用到三個八度:低音區、中音區、高音區。是以,你隻需要記住中音 1 的編号是 60 就好辦了。你看啊,中音 1 是 60,那麼,低音 1 就是 60 - 12 = 48,高音 1 就是 60 + 12 = 72,倍高音 1 就是 60 + 12*2 = 84,倍低音 1 就是 60 - 12*2 = 36。

下面老周給你一張表,用以參考。

【Win 10 應用開發】MIDI 音樂合成——音符消息篇

音符消息的第三個位元組是音速,值從 0 - 127,這個所謂的音速,發送到裝置後實際表現出來的效果是音量,127時音量最大,如果是0就無聲了。如果我們向 MIDI 裝置發送一條音速 = 0 的 Note on 消息,它的結果等同于 Note off 消息。說白了就是,音速為 0 的 note on 消息等同于 note off 消息,結果都是停止播放音符。

舉幾個例子,如果要讓通道0發出中音 1 的聲音,首先,note on 的标志是 0x9,通道為0,合起來第一個位元組是 0x90;第二個位元組表示音符,中音1是60,即 0x3C; 第三個位元組是音速,我們用最大值127,即 0x7F。是以這條 note on 消息就是:

0x90  0x3C  0x7F      

要是想停止上面的音符,就發送:

0x80  0x3C  0x7F      

因為 Note Off 消息是停止音符的,是以音速值可以随便,這裡我還是用 127 吧。

再比如,向通道14發送一條播放中音 5 的消息。Note On 的标志是 0x9,通道 14 是 1110,即 0xE;中音 5 是 67,即 0x43;音速用最大值,是以,整條消息為:

0x9E  0x43  0x7F      

======================================================================

下面咱們開始程式設計,先說說連接配接裝置。不管是輸入還是輸出裝置,我們都可以用這種方法連接配接。

IMidiOutPort midiOuter = null;

        async Task<IMidiOutPort> GetOuterPortAsync()
        {
            // 擷取裝置查詢字元串
            string q = MidiOutPort.GetDeviceSelector();
            // 查找相關 MIDI 輸出裝置
            DeviceInformationCollection devs = await DeviceInformation.FindAllAsync(q);
            // 如果連接配接多個 MIDI 裝置,就要選一個來耍,
            // 如果沒有連外設,那隻能有一個,就是聲霸卡相容的合成器
            return await MidiOutPort.FromIdAsync(q);
        }      

然後初始化一下 out port。

midiOuter = await GetOuterPortAsync();      

不需要的時候,記得要清理一下。

midiOuter?.Dispose();      

這裡有一個很 TNND 重要的事情,一定要注意,聲明變量時,一定要聲明為 IMidiOutPort 接口類型,不要聲明為 MidiOutPort 類型,這樣做到時候很可能你無法與裝置通信,發了消息過去沒聲音。不要問為什麼了,記住就行,這是封裝 COM 元件的,COM通常都是用接口中來操作的。

好的,下面正式實作我們今天的示例,為了示範,老周特意寫了一首歌,意境優美,相當動聽,值得收藏。

【Win 10 應用開發】MIDI 音樂合成——音符消息篇

由于這首歌熱情揚溢,老周故意把節拍設定為 60,即每分鐘 60 拍,正好一秒一拍。

用來進行音樂合成,最好直接使用 MidiSynthesizer 類。

第一步。初始化。

MidiSynthesizer mSynthesizer = null;

        protected async override void OnNavigatedTo(NavigationEventArgs e)
        {
            mSynthesizer = await MidiSynthesizer.CreateAsync();
        }      

在離開目前頁面時,不再需要,釋放掉,洗地。

protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
        {
            mSynthesizer?.Dispose();
        }      

第二步,定義幾個變量,後面要用。

const int TEMPO = 1000; // 每秒一拍
        const byte CHANNEL = 0; // 通道0,本例隻用一個通道
        bool isPlaying = false;      

TEMPO 是節拍,咱們的曲子是 J = 60,故一秒一拍,這裡表示為 1000 毫秒。CHANNEL表示我們要用到的通道,為了簡單示範,我們這個示例隻用第一個 MIDI 通道,編号為 0。

isPlaying 防止重複播放,當正在播放時,它為 true,播放完後變為 false。

第三步,組合音符,并發送到 MIDI 裝置上。

if (isPlaying)
            {
                return;
            }

            isPlaying = true;
            // 播放音符
            MidiNoteOnMessage noteOn = null;
            // 停止音符
            MidiNoteOffMessage noteOff = null;

            // 組合音符清單
            List<Tuple<byte, int>> notes = new List<Tuple<byte, int>>();
            // 低音5 = 55,兩拍
            notes.Add(new Tuple<byte, int>(55, 2 * TEMPO));
            // 低音6 = 57,兩拍
            notes.Add(new Tuple<byte, int>(57, 2 * TEMPO));
            // 中音 3 = 64,一拍
            notes.Add(new Tuple<byte, int>(64, TEMPO));
            // 中音 2 = 62,一拍
            notes.Add(new Tuple<byte, int>(62, TEMPO));
            // 中音 3 = 64,一拍
            notes.Add(new Tuple<byte, int>(64, TEMPO));
            // 低音 6 = 57,一拍
            notes.Add(new Tuple<byte, int>(57, TEMPO));
            // 中音 3 = 64,半拍
            notes.Add(new Tuple<byte, int>(64, TEMPO / 2));
            // 低音 6 = 57,半拍
            notes.Add(new Tuple<byte, int>(57, TEMPO / 2));
            // 低音 6 = 57,一拍
            notes.Add(new Tuple<byte, int>(57, TEMPO));
            // 中音 1 = 60,兩拍
            notes.Add(new Tuple<byte, int>(60, 2 * TEMPO));
            // 中音 5 = 67,兩拍
            notes.Add(new Tuple<byte, int>(67, 2 * TEMPO));
            // 中音 3 = 64,一拍
            notes.Add(new Tuple<byte, int>(64, TEMPO));
            // 中音 1 = 60,一拍
            notes.Add(new Tuple<byte, int>(60, TEMPO));
            // 低音 7 = 59,半拍
            notes.Add(new Tuple<byte, int>(59, TEMPO / 2));
            // 中音 2 = 62,半拍
            notes.Add(new Tuple<byte, int>(62, TEMPO / 2));
            // 低音 5 = 55,一拍
            notes.Add(new Tuple<byte, int>(55, TEMPO));
            // 低音 7 = 59,一拍
            notes.Add(new Tuple<byte, int>(59, TEMPO));
            // 中音 2 = 62,一拍
            notes.Add(new Tuple<byte, int>(62, TEMPO));
            // 低音 7 = 59,一拍
            notes.Add(new Tuple<byte, int>(59, TEMPO));
            // 低音 6 = 57,一拍
            notes.Add(new Tuple<byte, int>(57, TEMPO));
            // 中音 1 = 60,兩拍
            notes.Add(new Tuple<byte, int>(60, 2 * TEMPO));

            // 開始操作
            foreach (var tp in notes)
            {
                // 開啟音符
                noteOn = new MidiNoteOnMessage(CHANNEL, tp.Item1, 127);
                // 發送
                mSynthesizer.SendMessage(noteOn);
                // 延時
                await Task.Delay(tp.Item2);
                // 停止
                noteOff = new MidiNoteOffMessage(CHANNEL, tp.Item1, 127);
                // 發送
                mSynthesizer.SendMessage(noteOff);
            }

            isPlaying = false;      

Tuple 是元組,以前老周在其他博文中說過,就是簡單地把兩個值組合起來,我們這裡用了兩種值,byte類型的表示音符編号,int類型的表示音符要持續的時間,即時值。

我先用一個 List 把所有的音符與時值組合起來,然後再通過一個循環來發送到聲霸卡。

注意,在發送完 Note On後,不能立即發 Note Off,因為那樣音符會停止,你就聽不到了,是以要用 Delay 方法延時一下,而延時的時間就是音符的時值。如果是一拍,就是 1000 毫秒,如果是兩拍就是 2000 毫秒,如果是半拍,就是 500 毫秒……

第四步,現在雖然代碼已經寫完了,但你是無法合成 MIDI 音樂的,因為 MIDI API 是微軟為我們封裝過的,咱們還需要添加一個引用。如下圖,請勾選【Microsoft General MIDI DLS for Universal Windows Apps】,注意是勾上前面的對勾,不要隻選中,最後點确定即可。

【Win 10 應用開發】MIDI 音樂合成——音符消息篇

現在,運作應用,然後點選【演奏這首歌】按鈕,就能聽到了。

你聽到的是大鋼琴的聲音,因為這是預設音色。通用音色庫可以使用 128 種樂器音色,這個老周将在下一篇中介紹。

本篇示例源代碼,請猛點選這裡下載下傳。

繼續閱讀