在開始今天的吹 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);
}
}
這樣就大功告成了,運作試試吧。
示例源代碼下載下傳位址