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

这一篇老周将给你讲述一下如何让你的声卡播放一个音符,这会包含两条消息,而且这两条消息是很常用的。

1、Note On:让 MIDI 设备(如果没有专业设备,那就是你的声卡)发出某个音符的声音,比如,发出中音 3 的声音。注意啊,Note on 一旦发送,设备会一直播放这个声音,要想停止播放一个音符,你就要用到下面这条消息,它们是天生的一对。

2、Note Off:关闭某个音符,即停止播放某个音符。

咱们先来了解三个很重要的类,跟 MIDI 设备通信相关的 API 都在 Windows.Devices.Midi 命名空间下,封装好的。

1、MidiInPort:用来从 MIDI 输入设备接收消息,所以它公开了一个 MessageReceived 事件,只要 MIDI 输入设备发送了消息,就会引发这个事件,这时候你可以处理这个事件,把收到的消息再传到声卡上进行播放。MIDI 输入设备一般是 MIDI 键盘,估计大部分人用不上这个类,因为一般人不会购买 MIDI 键盘。真想买个好用的,起码是 88 键的,价格还是不低的。

2、MidiOutPort:连接 MIDI 输出设备,可以播放 MIDI 音乐。如果没有专业的 MIDI 音响,就可以连到你的声卡上,内置外置都可以,市面上有外置的 MIDI 声卡卖,当然了,想省钱的话,你是买不到好音色的,要是你不在乎音色的话,那无所谓。

3、MidiSynthesizer:这个类非常好使,它其实类似于 MidiOutPort 类,但它可以自动选择默认的设备(当然也可选择设备)。这个类是专门针对 MIDI 合成而设计的,尽管它与 MidiOutPort 相似,但侧重点不同。MidiOutPort 侧重于与 MIDI 设备的通信,而 MidiSynthesizer 类是侧重于合成。

我们在进行电子音乐合成的时候,只需要使用 MidiSynthesizer 类即可,它没有构造函数,可以调用 CreateAsync 静态方法来获取实例。对于普通设备而言,我们调用无参数的重载版本就行了,应用程序会默认选择声卡作为输出设备。然后,我们尽管发送 MIDI 消息就OK。当不再使用 MidiSynthesizer 实例时,应该把它 Dispose 掉,以释放资源占用。

是不是很简单呢,一切都是封装好的,所以说,你只要有一定的乐理基础就可以轻松玩耍这些 API。据说,这个 MidiSynthesizer 类还包含了罗兰公司(Roland)的通用音色库。

当然了,这只能是通用的 128 种乐器的声音,不包含各种演奏技巧(如揉弦、波音、颤音等)。其目的是尽可能地兼容各类声卡,包括很烂的声卡,虽然比较普通,不过嘛,音色听着还是可以的,只是少了点感觉。不过也是,电声毕竟是虚假的乐音,而不是自然音,就算是专业级别的音源,其实听着也不会太有乐感的。所以嘛,真想感受音乐之美,还是买个真实的乐器自己去演奏。老周小时候喜欢口琴和笛子,上初中的时候,学了一点电子琴、口风琴和扬琴,不过只是学了一点点而已。上高中的三年基本没碰过乐器。大学的时候,在学生会里面鬼混,所以经常可以拿乐队的吉他拨两下。

后来,像洞箫、巴乌、葫芦丝、陶埙、陶笛等都学过。想学学古琴,但是买一把好琴比较贵,就没有去学了。吹奏类乐器一般比较便宜,至少像老周这种穷人还能买得起,因此老周家里放的乐器,多数是吹奏类的。击打类的有一对小铜鼓,在路边捡的。

好,不扯了,咱们说正题。本篇的重点是学会两条 MIDI 消息,对,就是上面说的 Note on 和 Note off。不管是 on 还是 off,这两条音符消息的格式是一样的,都是包含三个字节。

第一个字节是 【状态码 + 通道编号】,这个可能你不太理解,没事,老周待会儿再解释。

第二个字节是音符,对,就是上一篇中,简谱上面的 1234567,唱出来就是 dol re mi fa sol la xi,用一个字节表示,从 0 - 127,共128 个音符。

第三个字节是音速,值也是从 0 到 127。这个音速其实你感觉不到什么,发送到声卡上的效果就是音量。值越小声音越小,如果是 0 就等于静音了,127 时声音最大。

好,下面逐个解释两下。

首先,状态码,在前一篇中,老周简单地说了一下 MIDI 文件的结构,一个 MIDI 事件是由 delta-time 和事件主体组成。而一个事件的开头都有一个标志字节。在MIDI文件中, Note on 和 Note off 都是一个事件;而在实时通信中,可认为是一条 MIDI 消息,其实结构是一样的。

不管是Note on 和 Note off ,还是其他通道消息,其第一个字节是由两部分信息组成的。我们知道,一个字节有 8 位,从右边起,1 - 4位表示通道编号,所以,MIDI 音乐有 16 个通道。为什么是 16 个通道呢,不是刚说了吗,只有 4 位二进制位表示通道编号,二进制 1111 就是 15,所以,通道的有效编号是 0 - 15,共16个。

注意:轨道与通道不同。轨道地用于 MIDI 文件的,可以是单轨,可以是多轨,轨道只是方便存储,也方便人类查看,但 MIDI 设置并不认轨道,只认识标准的 16 个通道。故 MIDI 消息只有通道的概念。另外,还要注意,第 10 个通道(编号 9 )是打击乐专用通道,在 GM 2 标准中,增加了一个,即第 10、11 通道可用于打击乐(编号 9、10)。

第 5 到 8 位表示状态码,或者说事件标志,总之,用来标识某个指令。Note Off 的标志是 1000,换算为十六进制就是 0x8 ;Note On 的标志是 1001,换算为十六进制就是 0x9。

假设,要向第四个通道发送一条 Note on 消息。第四个通道的编号是 3,换算为二进制就是 0011,Note on 的标志为 1001,所以,组合起来,第一个字节就是 1001 0011,换算为十六进制就是 0x93。再比如,要向第一个通道发送一条消息,第一通道的编号是0,即 0000,Note on 的标志是 1001,组合起来的字节就是 1001 0000,换算为十六进制就是 0x90。

如果要向第二个通道发送一条 Note off 消息。第二个通道的编号是 1,即 0001,Note off 的标志为 1000,组合起来的字节就是 0x81。

音符消息的第二个字节是音符,值从 0 - 127,共128个。虽然有 128 个音符,但实际上你只要记住一个值就行了—— 60,它表示的是中音 1 。128 / 12,余数为 8 ,凑不成一个 12,所以,中音 1 就位于 120 / 2 = 60 处。为什么音符是 12 个一组呢?上一篇中老周为啥要介绍“十二平均律”,就是有用的,MIDI 的音符排序是遵守十二平均律的,所以每 12 个音符构成一个“八度”。

于是这一来,这里头就有十来个八度了,其实我们大多数歌曲根本用不上,很多情况下,只用到三个八度:低音区、中音区、高音区。所以,你只需要记住中音 1 的编号是 60 就好办了。你看啊,中音 1 是 60,那么,低音 1 就是 60 - 12 = 48,高音 1 就是 60 + 12 = 72,倍高音 1 就是 60 + 12*2 = 84,倍低音 1 就是 60 - 12*2 = 36。

下面老周给你一张表,用以参考。

音符消息的第三个字节是音速,值从 0 - 127,这个所谓的音速,发送到设备后实际表现出来的效果是音量,127时音量最大,如果是0就无声了。如果我们向 MIDI 设备发送一条音速 = 0 的 Note on 消息,它的结果等同于 Note off 消息。说白了就是,音速为 0 的 note on 消息等同于 note off 消息,结果都是停止播放音符。

举几个例子,如果要让通道0发出中音 1 的声音,首先,note on 的标志是 0x9,通道为0,合起来第一个字节是 0x90;第二个字节表示音符,中音1是60,即 0x3C; 第三个字节是音速,我们用最大值127,即 0x7F。所以这条 note on 消息就是:

0x90  0x3C  0x7F

要是想停止上面的音符,就发送:

0x80  0x3C  0x7F

因为 Note Off 消息是停止音符的,所以音速值可以随便,这里我还是用 127 吧。

再比如,向通道14发送一条播放中音 5 的消息。Note On 的标志是 0x9,通道 14 是 1110,即 0xE;中音 5 是 67,即 0x43;音速用最大值,所以,整条消息为:

0x9E  0x43  0x7F

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

下面咱们开始编程,先说说连接设备。不管是输入还是输出设备,我们都可以用这种方法连接。

        IMidiOutPort midiOuter = null;async Task<IMidiOutPort> GetOuterPortAsync(){// 获取设备查询字符串string q = MidiOutPort.GetDeviceSelector();// 查找相关 MIDI 输出设备DeviceInformationCollection devs = await DeviceInformation.FindAllAsync(q);// 如果连接多个 MIDI 设备,就要选一个来耍,// 如果没有连外设,那只能有一个,就是声卡兼容的合成器return await MidiOutPort.FromIdAsync(q);}

然后初始化一下 out port。

  midiOuter = await GetOuterPortAsync();

不需要的时候,记得要清理一下。

  midiOuter?.Dispose();

这里有一个很 TNND 重要的事情,一定要注意,声明变量时,一定要声明为 IMidiOutPort 接口类型,不要声明为 MidiOutPort 类型,这样做到时候很可能你无法与设备通信,发了消息过去没声音。不要问为什么了,记住就行,这是封装 COM 组件的,COM通常都是用接口中来操作的。

好的,下面正式实现我们今天的示例,为了演示,老周特意写了一首歌,意境优美,相当动听,值得收藏。

由于这首歌热情扬溢,老周故意把节拍设置为 60,即每分钟 60 拍,正好一秒一拍。

用来进行音乐合成,最好直接使用 MidiSynthesizer 类。

第一步。初始化。

        MidiSynthesizer mSynthesizer = null;protected async override void OnNavigatedTo(NavigationEventArgs e){mSynthesizer = await MidiSynthesizer.CreateAsync();}

在离开当前页面时,不再需要,释放掉,洗地。

        protected override void OnNavigatingFrom(NavigatingCancelEventArgs e){mSynthesizer?.Dispose();}

第二步,定义几个变量,后面要用。

        const int TEMPO = 1000; // 每秒一拍const byte CHANNEL = 0; // 通道0,本例只用一个通道bool isPlaying = false;

TEMPO 是节拍,咱们的曲子是 J = 60,故一秒一拍,这里表示为 1000 毫秒。CHANNEL表示我们要用到的通道,为了简单演示,我们这个示例只用第一个 MIDI 通道,编号为 0。

isPlaying 防止重复播放,当正在播放时,它为 true,播放完后变为 false。

第三步,组合音符,并发送到 MIDI 设备上。

            if (isPlaying){return;}isPlaying = true;// 播放音符MidiNoteOnMessage noteOn = null;// 停止音符MidiNoteOffMessage noteOff = null;// 组合音符列表List<Tuple<byte, int>> notes = new List<Tuple<byte, int>>();// 低音5 = 55,两拍notes.Add(new Tuple<byte, int>(55, 2 * TEMPO));// 低音6 = 57,两拍notes.Add(new Tuple<byte, int>(57, 2 * TEMPO));// 中音 3 = 64,一拍notes.Add(new Tuple<byte, int>(64, TEMPO));// 中音 2 = 62,一拍notes.Add(new Tuple<byte, int>(62, TEMPO));// 中音 3 = 64,一拍notes.Add(new Tuple<byte, int>(64, TEMPO));// 低音 6 = 57,一拍notes.Add(new Tuple<byte, int>(57, TEMPO));// 中音 3 = 64,半拍notes.Add(new Tuple<byte, int>(64, TEMPO / 2));// 低音 6 = 57,半拍notes.Add(new Tuple<byte, int>(57, TEMPO / 2));// 低音 6 = 57,一拍notes.Add(new Tuple<byte, int>(57, TEMPO));// 中音 1 = 60,两拍notes.Add(new Tuple<byte, int>(60, 2 * TEMPO));// 中音 5 = 67,两拍notes.Add(new Tuple<byte, int>(67, 2 * TEMPO));// 中音 3 = 64,一拍notes.Add(new Tuple<byte, int>(64, TEMPO));// 中音 1 = 60,一拍notes.Add(new Tuple<byte, int>(60, TEMPO));// 低音 7 = 59,半拍notes.Add(new Tuple<byte, int>(59, TEMPO / 2));// 中音 2 = 62,半拍notes.Add(new Tuple<byte, int>(62, TEMPO / 2));// 低音 5 = 55,一拍notes.Add(new Tuple<byte, int>(55, TEMPO));// 低音 7 = 59,一拍notes.Add(new Tuple<byte, int>(59, TEMPO));// 中音 2 = 62,一拍notes.Add(new Tuple<byte, int>(62, TEMPO));// 低音 7 = 59,一拍notes.Add(new Tuple<byte, int>(59, TEMPO));// 低音 6 = 57,一拍notes.Add(new Tuple<byte, int>(57, TEMPO));// 中音 1 = 60,两拍notes.Add(new Tuple<byte, int>(60, 2 * TEMPO));// 开始操作foreach (var tp in notes){// 开启音符noteOn = new MidiNoteOnMessage(CHANNEL, tp.Item1, 127);// 发送
                mSynthesizer.SendMessage(noteOn);// 延时await Task.Delay(tp.Item2);// 停止noteOff = new MidiNoteOffMessage(CHANNEL, tp.Item1, 127);// 发送
                mSynthesizer.SendMessage(noteOff);}isPlaying = false;

Tuple 是元组,以前老周在其他博文中说过,就是简单地把两个值组合起来,我们这里用了两种值,byte类型的表示音符编号,int类型的表示音符要持续的时间,即时值。

我先用一个 List 把所有的音符与时值组合起来,然后再通过一个循环来发送到声卡。

注意,在发送完 Note On后,不能立即发 Note Off,因为那样音符会停止,你就听不到了,所以要用 Delay 方法延时一下,而延时的时间就是音符的时值。如果是一拍,就是 1000 毫秒,如果是两拍就是 2000 毫秒,如果是半拍,就是 500 毫秒……

第四步,现在虽然代码已经写完了,但你是无法合成 MIDI 音乐的,因为 MIDI API 是微软为我们封装过的,咱们还需要添加一个引用。如下图,请勾选【Microsoft General MIDI DLS for Universal Windows Apps】,注意是勾上前面的对勾,不要只选中,最后点确定即可。

现在,运行应用,然后点击【演奏这首歌】按钮,就能听到了。

你听到的是大钢琴的声音,因为这是默认音色。通用音色库可以使用 128 种乐器音色,这个老周将在下一篇中介绍。

本篇示例源代码,请猛点击这里下载。

【Win 10 应用开发】MIDI 音乐合成——音符消息篇相关推荐

  1. 【Win 10 应用开发】Toast通知激活应用——前台后台

    原文:[Win 10 应用开发]Toast通知激活应用--前台&后台 老周最近热衷于讲故事,接下来还是讲故事时间. 有人问我:你上大学的时候,有加入过学生会吗?读大学有没有必要加入学生会? 哎 ...

  2. 【Win 10应用开发】SplitView控件

    [Win 10应用开发]SplitView控件 原文:[Win 10应用开发]SplitView控件 SplitView控件用于呈现分隔视图,简单地说,就是把一个视图分割为两部分,Content属性所 ...

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

    原文:[Win 10 应用开发]RTM版的UAP项目解剖 Windows 10 发布后,其实SDK也偷偷地在VS的自定义安装列表中出现了,今天开发人员中心也更新了下载.正式版的SDK在API结构上和以 ...

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

    原文:[Win 10应用开发]Adaptive磁贴模板的XML文档结构 在若干天之前,老周给大家讲了Adaptive Toast通知的XML模板,所以相应地,今天老周给大家介绍一下Adaptive磁贴 ...

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

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

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

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

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

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

  8. 【Win 10 应用开发】InkToolBar——涂鸦如此简单

    从WPF开始,就有个InkCanvas控件,封装了数字墨迹处理相关的功能,Silverlight到Win 8 App,再到UWP应用,都有这个控件,所以,老周说了3688遍,凡是.net基础扎实者,必 ...

  9. 【Win 10 应用开发】将墨迹保存到图像的两种方法

    IT界最近这几年,各种乱七八糟的东西不断出现,其中能用在实际工作与生活中的,大概也就那么几个.Web 前端也冒出各种框架,这就为那些喜欢乱用框架的公司提供了很好的机会,于是造成很多项目体积越来越庞大, ...

最新文章

  1. Android 双屏开发 Presentation 的使用教程
  2. ABAP术语-Object Name
  3. The connected J-Link is defective,Proper operation cannot be guaranteed......的解决办法
  4. Windoes普通用户使用管理员下安装的软件
  5. OpenGL:画个水壶
  6. (81)Vivado实现约束过程
  7. 语音识别(3)---语音识别技术原理
  8. 新松机器人BG总裁高峰_青春,就是用来奋斗的——记沈阳新松机器人自动化股份有限公司创始人兼总裁曲道奎...
  9. 查询公司资产数据库中的主机是否在zabbix的监控中
  10. c语言正则表达式替换,Linux C 支持正则表达式的字符串替换函数...
  11. 华为云计算IE面试笔记-云磁盘和普通磁盘的区别。
  12. Android5.0更新内容简介
  13. 如何让MAC只显示外接屏幕!
  14. 快速学习一门技术的逻辑
  15. linux进阶-scp命令及相关传输命令全get
  16. 单击Artwork时没有弹窗的问题
  17. 如何快速写出Json Schema,校验Json Schema
  18. 再探矩阵求逆引理 : Woodbury恒等式的证明
  19. 【VMware】ubuntu运行VMware kernel module updater
  20. 《数字电子技术课程设计》课程笔记(二)————multisim仿真模拟

热门文章

  1. “真功夫”与“花拳绣腿”
  2. IFD-x 微型红外成像仪(模块)的温度测量和成像精度
  3. Vue 按enter键实现登陆 过程
  4. 接着,运营基础知识(福利篇)
  5. OpenGL LookAt、Camera摄像机
  6. ▼ 系列 | 漫谈数仓第四篇NO.4 『BI选型』
  7. Linux中GCC编译工具集中个软件的用途、gcc的简单编译以及ELF文件格式
  8. Oracle EBS MRP模块之预测冲减
  9. LPC55S69开发笔记
  10. php处理表单上传文件