在开始今天的吹 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(, tp.Item1, ));
await Task.Delay(tp.Item2);
synthesizer.SendMessage(new MidiNoteOffMessage(, tp.Item1, ));
} 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 == -) return;
if (isPlaying) return; // 更改音色一般在发送音符之前发送
// 不必每个音符都发送 ProgramChange 消息
// 它会自动保持,直到发送下一条 ProgramChange 消息 // 获得列表框中选中的音色编号
ListBoxItem item = lbProgram.SelectedItem as ListBoxItem;
byte pc = Convert.ToByte(item.Tag);
// 发送更改音色消息
MidiProgramChangeMessage pcmsg = new MidiProgramChangeMessage(, pc);
// 这个示例只使用第一个通道,你也可以视不同情况使用其他通道
synthesizer.SendMessage(pcmsg); double tempo = / * ;//节奏
// 开始发送音符
List<Tuple<byte, TimeSpan>> notelist = new List<Tuple<byte, TimeSpan>>();
// 第一句
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 2d)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 2d))); notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 2d))); // 后面是两个休止符,我们可以用音符 0
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 2d))); // 第二句
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));
// 以下音符有附点,时值为一拍,再延长原时值的一半,即 1.5 拍
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 1.5d)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 1.5d)));//4 附点
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//低音7
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 2d)));// 0 // 第三句
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo))); //
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));//高音1
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 2d)));//高音1
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 2d)));//高音1
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//高音1
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 2d)));//2
// 休止
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 2d))); // 最后一句
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 1.5d)));//高音1
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));//高音1
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo * 1.5d)));//4 附点
notelist.Add(new Tuple<byte, TimeSpan>(, TimeSpan.FromMilliseconds(tempo / 2d)));//低音7
notelist.Add(new Tuple<byte, TimeSpan>(, 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 == )
{
await Task.Delay(ti.Item2);
continue;
}
await PlaySingleNoteAsync(ti);
}
}

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

示例源代码下载地址

【Win 10 应用开发】MIDI 音乐合成——更改乐器音色的更多相关文章

  1. 【Win 10 应用开发】MIDI 音乐合成——音符消息篇

    在上一篇中,老周介绍了一些乐理知识,有了那些常识后,进行 MIDI 编程就简单得多了.尽管微软已经把 API 封装好,用起来也很简单,但是,如果你没有相应的音乐知识基础,你是无法进行 MIDI 编程的 ...

  2. 【Win 10 应用开发】MIDI 音乐合成——乐理篇

    针对 MIDI 音乐的 API ,其实在 Win 8.1 的时候就出现.在UWP中采用了新的驱动模式,MIDI 消息传递更加高效. 首先得说明的是,UWP 的 MIDI 相关 API 不是针对 MID ...

  3. 【Win 10 应用开发】在代码中加载文本资源

    记得前一次,老周给大伙,不,小伙伴们介绍了如何填写 .resw 文件,并且在 XAML 中使用 x:Uid 标记来加载.也顺便给大伙儿分析了运行时是如何解析 .resw 文件的. 本来说好了,后续老周 ...

  4. 【Win 10 应用开发】启动远程设备上的应用

    这个功能必须在“红石-1”(build 14393)以上的系统版中才能使用,运行在一台设备上的应用,可以通过URI来启动另一台设备上的应用.激活远程应用需要以下前提: 系统必须是build 14393 ...

  5. 【Win 10 应用开发】导入.pfx证书

    这个功能其实并不常用,一般开发较少涉及到证书,不过,简单了解一下还是有必要的. 先来说说制作测试证书的方法,这里老周讲两种方法,可以生成用于测试的.pfx文件. 产生证书,大家都知道有个makecer ...

  6. 【Win 10应用开发】Adaptive磁贴模板的XML文档结构

    在若干天之前,老周给大家讲了Adaptive Toast通知的XML模板,所以相应地,今天老周给大家介绍一下Adaptive磁贴的新XML模板. 同样道理,你依旧可以使用8.1时候的磁贴模板,在win ...

  7. 【Win 10 应用开发】RTM版的UAP项目解剖

    Windows 10 发布后,其实SDK也偷偷地在VS的自定义安装列表中出现了,今天开发人员中心也更新了下载.正式版的SDK在API结构上和以前预览的时候是一样的,只是版本变成10240罢了,所以大家 ...

  8. 【Win 10应用开发】认识一下UAP项目

    Windows 10 SDK预览版需要10030以上版本号的Win 10预览版系统才能使用.之前我安装的9926的系统,然后安装VS 2015 CTP 6,再装Win 10 SDK,但是在新建项目后, ...

  9. 【Win 10应用开发】延迟共享

    延迟共享是啥呢,这么说吧,就是在应用程序打开共享面板选择共享目标时,不会设置要共享的数据,而是等到共享目标请求数据时,才会发送数据,而且,延迟操作可以在后台进行. 这样说似乎过于抽象,最好的诠释方法, ...

随机推荐

  1. 11g使用非duplicate方式创建物理standby要注意的问题总结

    在上篇博文中,使用了duplicate方式来创建物理standby http://blog.csdn.net/aaron8219/article/details/38434579 今天来说说在11g中 ...

  2. EBS採购模块中的级联接收和级联接收事务

    EBS採购模块中的级联接收和级联接收事务 (版权声明,本人原创或者翻译的文章如需转载,如转载用于个人学习.请注明出处:否则请与本人联系.违者必究) 级联接收和级联接收事务 级联功能对来自于同一个供应商 ...

  3. Python 爬虫 爬校花网!!

    爬虫:是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本 1.福利来了  校花网 ,首先说为什么要爬这个网站呢,第一这个网站简单爬起来容易不会受到打击,第二呢 你懂得... 1.第一步,需要下载 ...

  4. Android中build target,minSdkVersion,targetSdkVersion,maxSdkVersion概念区分

    问题引入:   有一个下载功能,在Android 5.x设备上运行正常,Android 6.x上运行异常,现象是下载进度卡在0%.   问题排查发现,是sdk的target设置为23导致,修改为21则 ...

  5. 关于python的itertools模块

    这是一个强大的模块 先来看一下它都有什么工具 无穷循环器 迭代器         参数         结果                                               ...

  6. Github开源项目(企业信息化基础平台)

    JEEPlatform 一款企业信息化开发基础平台,可以用于快速构建企业后台管理系统,集成了OA(办公自动化).SCM(供应链系统).ERP(企业资源管理系统).CMS(内容管理系统).CRM(客户关 ...

  7. 这么说吧,java线程池的实现原理其实很简单

    好处 : 线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的使用线程池对线程进行统一分配.调优和监控,有以下好处: 1.降低资源消耗: 2.提高响应速度: 3.提高线 ...

  8. 常见.NET功能代码汇总 (3)

    33,彻底关闭Excel进程 .NET中使用Excel属于使用非托管资源,使用完成后一般都要用GC回收资源,但是,调用GC的位置不正确,Excel进程可能无法彻底关闭,如下面的代码: static v ...

  9. 《TCP-IP详解卷3:TCP 事务协议、HTTP、NNTP和UNIX域协议》【PDF】下载

    TCP-IP详解卷3:TCP 事务协议.HTTP.NNTP和UNIX域协议>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230062539 ...

  10. 简述ES6其他的东西

    第一是修饰器是ES7的一个提案,现在Babel转码器已经支持.那么什么是修饰器呢,修饰器是对类的行为的改变,在代码编译时发生的,而不是在运行时发生的且修饰器只能用于类和类的方法.修饰器可以接受三个函数 ...