天天看點

【Win 10 應用開發】MIDI 音樂合成——更改樂器音色

在開始今天的吹 BB 博文之前,說點題外話。

首先,上次老周給大夥伴們介紹完發送 MIDI 音符,本來說好的接着說一下如何更改樂器音色,為啥這麼久都沒更新呢。特特來解釋一下,最近老周接了一個 ASP.NET Core 的項目,是以忙碌了一段時間。項目不大,一個人獨立完成的話感覺特好。

其次,族中一位兄弟大學畢業了,他一直想找一個網頁前端的。然後他看到許多招聘資訊上寫着要求你精通1、2、3、4、5、6、7、8、9、10、11、12、…… 一大堆架構。然後他問我,哥,你能精通那些架構嗎?

我回答:能,我精通各大搜尋引擎,隻要有搜尋引擎,每個架構我可以三分鐘學會,然後直接運用,用完直接忘記。人類曆史上最無恥的招聘資訊就是用“精通”二字。老周也說過,這些公司都是神經病高發群體。

說到底,病根在于浮躁,其實你隻要基礎紮實,什麼東西你都可以現學現用,用完忘記。就算明年再出現十個 JS 架構也無妨,還是老規矩,用的時候學,學完就用,用完扔掉。比如,Bootstrap 老周就是這樣的,做頁面要排版,用起來挺友善,于是直接進他官網,看完文檔看示例,看完示例 Run 一下。然後直接用到項目中,用完之後呢,忘了。

很多時候,負擔都是你自己給自己創造的,心理壓力也是自己折騰出來的。

看到現在很多畢業生求職,又想起老周當年。求職千萬不要緊張,也不要睡不着覺,車到山前必有路,走出個通天大道寬又闊。總能找到活幹的,放心好了。同時,也不要因為自己是畢業生,就總覺得自己滿身是劣勢,甚至被面試官問幾句就很慌張。

不用怕的,面試人員算個啥,他又不敢吃了你,你怕啥。心情不好的時睺,你也可以拿面試官來出出氣的。記得 2011 年換工作的時候,老周也戲弄過面試官。很搞笑的是,我戲弄他,他居然錄用了我。反正,他問啥我都能答,全是胡說八道。忽悠是一項雙向社會工程,你忽悠我,我忽悠你,各得其樂罷了。企業忽悠員工,員工忽悠企業,企業忽悠媒體,媒體忽悠社會公衆——忽悠生态鍊。

哦,是了,上面提到了做 ASP.NET Core 項目,這個其實比傳統的 ASP.NET 還要簡單,雖然跨平台了,但風格依然很微軟的,傳承了微軟的優良基因——簡單易用效率高。.net Core 的内容網上很多,老周也不細說了,最近一兩年,到處都是 Core 在刷屏,教程非常的多,隻要你基礎硬,哪怕不看其他教程,隻看官方文檔,一小時就能學會。

這裡老周提一下的時,在Linux上測試時,可能你會想到在虛拟機裡裝 Linux 系統。其實根本不用,虛拟機不僅消耗性能,而且也折騰。最簡單高效的方法就是啟用 Windows 10 的 Linux 子系統(Bash功能),然後你到應用商店安裝一下 Ubuntu 或者其他兩個版本。這個子系統很 TNND 好用,而且可以直接通路 Windows 目錄和檔案,用來測試 ASP.NET Core 項目非常友善。

如果你不熟悉 Linux 不知道怎麼弄,沒關系,後面老周會寫一篇爛文,詳細告訴你怎麼玩,放心吧,很簡單的,你了解老周的,老周從來不寫那些鬼都看不懂的東西。不過,今天的主題還是繼續咱們的 MIDI 合成。

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

好,F 話說得太多了,擔心有人會扔磚頭,老周并不怕被磚砸到,是擔心你不知道從哪個考古發掘現場偷來的磚,這容易引起法律責任,偷文物是不文明的。

是以的 MIDI 通道消息都有共同特點,由兩到三個位元組組成,大部分是三個位元組,個别是兩個位元組,比如本文要介紹的這個更改樂器音色的 Program Change 消息,它就是兩個位元組組成的。

所有通道消息的第一個位元組都有兩部分組成,我們知道一個位元組是 8 位,狀态碼占高 4 位,辨別消息類型;通道編号占低 4 位。

Program Change 消息的狀态碼(或者說指令辨別碼)是 1100 ,這是二進制,十六進制是 0xC。然後我們前面說過,通道是 0 到 15 共十六個,即 0x0 - 0xF。于是,兩個合起來正好是一個位元組,比如我要更改第一個通道上的音色,Program Change 消息的第一個位元組就是 0xC0,如果要改第二個通道上的音色,就是 0xC1。

第二個位元組表示樂器的編号,隻使用1-7位,是以有效值為 0 - 127,共 128 種音色。

由于 UWP SDK 已經封裝好 MidiProgramChangeMessage 類,是以用的時候,你不需要記憶狀态碼,構造執行個體時, 你隻提供兩個位元組就行了,第一個是能道編号,第二個是音色編号。

128 種音色清單你可以到 midi.org 上檢視,如果你嫌洋鬼子的文字看不懂,那行,老周給你整理了一下。如果你覺得無聊,可以直接看後面的示例。

第一個表格,是說樂器的分類,如吹管類的,撥弦類的,打擊類的。

【Win 10 應用開發】MIDI 音樂合成——更改樂器音色
 第二個表是樂器的清單。
【Win 10 應用開發】MIDI 音樂合成——更改樂器音色
【Win 10 應用開發】MIDI 音樂合成——更改樂器音色
【Win 10 應用開發】MIDI 音樂合成——更改樂器音色
【Win 10 應用開發】MIDI 音樂合成——更改樂器音色
【Win 10 應用開發】MIDI 音樂合成——更改樂器音色
【Win 10 應用開發】MIDI 音樂合成——更改樂器音色

注意啊,上面清單是從 1 開始的,我們在寫代碼時要從 0 開始,到 127。就是上面的編号 - 1。

其實是很簡單的,一般我們不需要播放每個音符都發送 ProgramChange 消息,什麼時候要改音色,就發送一條就行了,後面播放的音符都會應用這個更改,直到你再發送 ProgramChange 消息去進行更改。

下面我們用弘一法師(李叔同)填詞的一首歌來做示例,這首歌咱們上學的時候都學過的——《送别》,“長亭外,古道邊,芳草碧連天……”。

【Win 10 應用開發】MIDI 音樂合成——更改樂器音色
下面我們在界面上用 ListBox 控件來顯示幾個樂器選項,老周并沒有寫上 128 種,僅僅是挑了幾個做示範。

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Image Margin="10" Source="/Assets/1.png"/>
        <StackPanel Grid.Column="1" Margin="10">
            <TextBlock Text="選擇一種樂器:" Margin="1,3"/>
            <ListBox Name="lbProgram" Height="280" SelectionMode="Single" >
                <ListBoxItem Tag="18">搖滾風琴</ListBoxItem>
                <ListBoxItem Tag="79">陶笛</ListBoxItem>
                <ListBoxItem Tag="56">小号</ListBoxItem>
                <ListBoxItem Tag="112">鈴铛</ListBoxItem>
            </ListBox>
            <Button Margin="2,25,0,0" Content="演奏此曲" Click="OnClick"/>
        </StackPanel>
    </Grid>      

然後我們在頁面類上聲明一下變量。

MidiSynthesizer synthesizer = null;
        bool isPlaying = false;      

跟上一篇中的例 子一樣,這個 bool 類型的變量是為了防避重複執行代碼用的。

然後初始化一下 MIDI 合成器,而且在離開頁面時清理一下。

protected async override void OnNavigatedTo(NavigationEventArgs e)
        {
            // 獲得執行個體
            synthesizer = await MidiSynthesizer.CreateAsync();
        }

        protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
        {
            // 釋放執行個體
            synthesizer?.Dispose();
            synthesizer = null;
        }      

接着,在頁面類中弄兩個自定義方法,友善後面調用。一個方法是開始 / 停止播放單個音符,另一個方法是播放一個音符清單。PlayNotesAsync 方法中會調用 PlaySingleNoteAsync 方法。

async Task PlaySingleNoteAsync(Tuple<byte, TimeSpan> tp)
        {
            synthesizer.SendMessage(new MidiNoteOnMessage(0, tp.Item1, 127));
            await Task.Delay(tp.Item2);
            synthesizer.SendMessage(new MidiNoteOffMessage(0, tp.Item1, 127));
        }

        async Task PlayNotesAsync(IEnumerable<Tuple<byte, TimeSpan>> notes)
        {
            foreach (var ti in notes)
            {
                await PlaySingleNoteAsync(ti);
            }
        }      

好,準備好這些,可以處理按鈕的 Click 事件,組裝音符清單了。

private async void OnClick(object sender, RoutedEventArgs e)
        {
            if (lbProgram.SelectedIndex == -1) return;
            if (isPlaying) return;

            // 更改音色一般在發送音符之前發送
            // 不必每個音符都發送 ProgramChange 消息
            // 它會自動保持,直到發送下一條 ProgramChange 消息

            // 獲得清單框中選中的音色編号
            ListBoxItem item = lbProgram.SelectedItem as ListBoxItem;
            byte pc = Convert.ToByte(item.Tag);
            // 發送更改音色消息
            MidiProgramChangeMessage pcmsg = new MidiProgramChangeMessage(0, pc);
            // 這個示例隻使用第一個通道,你也可以視不同情況使用其他通道
            synthesizer.SendMessage(pcmsg);

            double tempo = 60 / 80 * 1000;//節奏
            // 開始發送音符
            List<Tuple<byte, TimeSpan>> notelist = new List<Tuple<byte, TimeSpan>>();
            // 第一句
            notelist.Add(new Tuple<byte, TimeSpan>(67, TimeSpan.FromMilliseconds(tempo)));
            notelist.Add(new Tuple<byte, TimeSpan>(64, TimeSpan.FromMilliseconds(tempo / 2d)));
            notelist.Add(new Tuple<byte, TimeSpan>(67, TimeSpan.FromMilliseconds(tempo / 2d)));
            notelist.Add(new Tuple<byte, TimeSpan>(72, TimeSpan.FromMilliseconds(tempo * 2d)));
            notelist.Add(new Tuple<byte, TimeSpan>(69, TimeSpan.FromMilliseconds(tempo)));
            notelist.Add(new Tuple<byte, TimeSpan>(72, TimeSpan.FromMilliseconds(tempo)));
            notelist.Add(new Tuple<byte, TimeSpan>(67, TimeSpan.FromMilliseconds(tempo * 2d)));

            notelist.Add(new Tuple<byte, TimeSpan>(67, TimeSpan.FromMilliseconds(tempo)));
            notelist.Add(new Tuple<byte, TimeSpan>(60, TimeSpan.FromMilliseconds(tempo / 2d)));
            notelist.Add(new Tuple<byte, TimeSpan>(62, TimeSpan.FromMilliseconds(tempo / 2d)));
            notelist.Add(new Tuple<byte, TimeSpan>(64, TimeSpan.FromMilliseconds(tempo)));
            notelist.Add(new Tuple<byte, TimeSpan>(62, TimeSpan.FromMilliseconds(tempo / 2d)));
            notelist.Add(new Tuple<byte, TimeSpan>(60, TimeSpan.FromMilliseconds(tempo / 2d)));
            notelist.Add(new Tuple<byte, TimeSpan>(62, TimeSpan.FromMilliseconds(tempo * 2d)));

            // 後面是兩個休止符,我們可以用音符 0
            notelist.Add(new Tuple<byte, TimeSpan>(0, TimeSpan.FromMilliseconds(tempo * 2d)));

            // 第二句
            notelist.Add(new Tuple<byte, TimeSpan>(67, TimeSpan.FromMilliseconds(tempo)));
            notelist.Add(new Tuple<byte, TimeSpan>(64, TimeSpan.FromMilliseconds(tempo / 2d)));
            notelist.Add(new Tuple<byte, TimeSpan>(67, TimeSpan.FromMilliseconds(tempo / 2d)));
            // 以下音符有附點,時值為一拍,再延長原時值的一半,即 1.5 拍
            notelist.Add(new Tuple<byte, TimeSpan>(72, TimeSpan.FromMilliseconds(tempo * 1.5d)));
            notelist.Add(new Tuple<byte, TimeSpan>(71, TimeSpan.FromMilliseconds(tempo / 2d)));
            notelist.Add(new Tuple<byte, TimeSpan>(69, TimeSpan.FromMilliseconds(tempo)));
            notelist.Add(new Tuple<byte, TimeSpan>(72, TimeSpan.FromMilliseconds(tempo)));
            notelist.Add(new Tuple<byte, TimeSpan>(67, TimeSpan.FromMilliseconds(tempo * 2d)));//5
            notelist.Add(new Tuple<byte, TimeSpan>(67, TimeSpan.FromMilliseconds(tempo)));//5
            notelist.Add(new Tuple<byte, TimeSpan>(62, TimeSpan.FromMilliseconds(tempo / 2d)));//2
            notelist.Add(new Tuple<byte, TimeSpan>(64, TimeSpan.FromMilliseconds(tempo / 2d)));//3
            notelist.Add(new Tuple<byte, TimeSpan>(65, TimeSpan.FromMilliseconds(tempo * 1.5d)));//4 附點
            notelist.Add(new Tuple<byte, TimeSpan>(59, TimeSpan.FromMilliseconds(tempo / 2d)));//低音7
            notelist.Add(new Tuple<byte, TimeSpan>(60, TimeSpan.FromMilliseconds(tempo * 2d)));//1
            notelist.Add(new Tuple<byte, TimeSpan>(0, TimeSpan.FromMilliseconds(tempo * 2d)));// 0

            // 第三句
            notelist.Add(new Tuple<byte, TimeSpan>(69, TimeSpan.FromMilliseconds(tempo))); //6
            notelist.Add(new Tuple<byte, TimeSpan>(72, TimeSpan.FromMilliseconds(tempo)));//高音1
            notelist.Add(new Tuple<byte, TimeSpan>(72, TimeSpan.FromMilliseconds(tempo * 2d)));//高音1
            notelist.Add(new Tuple<byte, TimeSpan>(71, TimeSpan.FromMilliseconds(tempo)));//7
            notelist.Add(new Tuple<byte, TimeSpan>(69, TimeSpan.FromMilliseconds(tempo / 2d)));//6
            notelist.Add(new Tuple<byte, TimeSpan>(71, TimeSpan.FromMilliseconds(tempo / 2d)));//7
            notelist.Add(new Tuple<byte, TimeSpan>(72, TimeSpan.FromMilliseconds(tempo * 2d)));//高音1
            notelist.Add(new Tuple<byte, TimeSpan>(69, TimeSpan.FromMilliseconds(tempo / 2d)));//6
            notelist.Add(new Tuple<byte, TimeSpan>(71, TimeSpan.FromMilliseconds(tempo / 2d)));//7
            notelist.Add(new Tuple<byte, TimeSpan>(72, TimeSpan.FromMilliseconds(tempo / 2d)));//高音1
            notelist.Add(new Tuple<byte, TimeSpan>(69, TimeSpan.FromMilliseconds(tempo / 2d)));//6
            notelist.Add(new Tuple<byte, TimeSpan>(69, TimeSpan.FromMilliseconds(tempo / 2d)));//6
            notelist.Add(new Tuple<byte, TimeSpan>(67, TimeSpan.FromMilliseconds(tempo / 2d)));//5
            notelist.Add(new Tuple<byte, TimeSpan>(64, TimeSpan.FromMilliseconds(tempo / 2d)));//3
            notelist.Add(new Tuple<byte, TimeSpan>(60, TimeSpan.FromMilliseconds(tempo / 2d)));//1
            notelist.Add(new Tuple<byte, TimeSpan>(62, TimeSpan.FromMilliseconds(tempo * 2d)));//2
            // 休止
            notelist.Add(new Tuple<byte, TimeSpan>(0, TimeSpan.FromMilliseconds(tempo * 2d)));

            // 最後一句
            notelist.Add(new Tuple<byte, TimeSpan>(67, TimeSpan.FromMilliseconds(tempo)));//5
            notelist.Add(new Tuple<byte, TimeSpan>(64, TimeSpan.FromMilliseconds(tempo / 2d)));//3
            notelist.Add(new Tuple<byte, TimeSpan>(67, TimeSpan.FromMilliseconds(tempo / 2d)));//5
            notelist.Add(new Tuple<byte, TimeSpan>(72, TimeSpan.FromMilliseconds(tempo * 1.5d)));//高音1
            notelist.Add(new Tuple<byte, TimeSpan>(71, TimeSpan.FromMilliseconds(tempo / 2d)));//7
            notelist.Add(new Tuple<byte, TimeSpan>(69, TimeSpan.FromMilliseconds(tempo)));//6
            notelist.Add(new Tuple<byte, TimeSpan>(72, TimeSpan.FromMilliseconds(tempo)));//高音1
            notelist.Add(new Tuple<byte, TimeSpan>(67, TimeSpan.FromMilliseconds(tempo * 2d)));//5
            notelist.Add(new Tuple<byte, TimeSpan>(67, TimeSpan.FromMilliseconds(tempo)));//5
            notelist.Add(new Tuple<byte, TimeSpan>(62, TimeSpan.FromMilliseconds(tempo / 2d)));//2
            notelist.Add(new Tuple<byte, TimeSpan>(64, TimeSpan.FromMilliseconds(tempo / 2d)));//3
            notelist.Add(new Tuple<byte, TimeSpan>(65, TimeSpan.FromMilliseconds(tempo * 1.5d)));//4 附點
            notelist.Add(new Tuple<byte, TimeSpan>(59, TimeSpan.FromMilliseconds(tempo / 2d)));//低音7
            notelist.Add(new Tuple<byte, TimeSpan>(60, TimeSpan.FromMilliseconds(tempo * 2d)));//1

            // 開始播放
            isPlaying = true;
            await PlayNotesAsync(notelist);
            isPlaying = false;
        }      

還有一步很重要的,記得要添加一個擴充引用。

【Win 10 應用開發】MIDI 音樂合成——更改樂器音色

這首曲子裡面出現了休止符(0),你也許會想到發送 NoteOn 0 音符,對于部分樂器音色來說,0确實不發聲,可有部分是會發出低沉的聲音。上面的代碼在添加音符清單時,用 0 表示休止符。現在不妨修改一下 PlayNotesAsync 方法的代碼,跳過休止符,但是,該延時還是得延時,不然就達不到停頓的效果了。

async Task PlayNotesAsync(IEnumerable<Tuple<byte, TimeSpan>> notes)
        {
            foreach (var ti in notes)
            {
                // 跳過休止符
                if(ti.Item1 == 0)
                {
                    await Task.Delay(ti.Item2);
                    continue;
                }
                await PlaySingleNoteAsync(ti);
            }
        }      

這樣就大功告成了,運作試試吧。

示例源代碼下載下傳位址

繼續閱讀