【Win 10 应用开发】MIDI 音乐合成——更改乐器音色

2018-01-13 10:56:11来源:http://www.cnblogs.com/tcjiaan/p/8244214.html作者:老周博客—“易之道”人点击

分享

在开始今天的吹 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);
}
}

这样就大功告成了,运行试试吧。


示例源代码下载地址


最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台