在開始今天的吹 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 上查看,如果你嫌洋鬼子的文字看不懂,那行,老周給你整理了一下。如果你覺得無聊,可以直接看后面的示例。
第一個表格,是說樂器的分類,如吹管類的,撥弦類的,打擊類的。
第二個表是樂器的列表。
注意啊,上面列表是從 1 開始的,我們在寫代碼時要從 0 開始,到 127。就是上面的編號 - 1。
其實是很簡單的,一般我們不需要播放每個音符都發送 ProgramChange 消息,什么時候要改音色,就發送一條就行了,后面播放的音符都會應用這個更改,直到你再發送 ProgramChange 消息去進行更改。
下面我們用弘一法師(李叔同)填詞的一首歌來做示例,這首歌咱們上學的時候都學過的——《送別》,“長亭外,古道邊,芳草碧連天……”。
下面我們在界面上用 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; }
還有一步很重要的,記得要添加一個擴展引用。
這首曲子里面出現了休止符(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); } }
這樣就大功告成了,運行試試吧。