在上一篇中,老周介绍了一些乐理知识,有了那些常识后,进行 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 = ; // 每秒一拍
const byte CHANNEL = ; // 通道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>(, * TEMPO));
// 低音6 = 57,两拍
notes.Add(new Tuple<byte, int>(, * TEMPO));
// 中音 3 = 64,一拍
notes.Add(new Tuple<byte, int>(, TEMPO));
// 中音 2 = 62,一拍
notes.Add(new Tuple<byte, int>(, TEMPO));
// 中音 3 = 64,一拍
notes.Add(new Tuple<byte, int>(, TEMPO));
// 低音 6 = 57,一拍
notes.Add(new Tuple<byte, int>(, TEMPO));
// 中音 3 = 64,半拍
notes.Add(new Tuple<byte, int>(, TEMPO / ));
// 低音 6 = 57,半拍
notes.Add(new Tuple<byte, int>(, TEMPO / ));
// 低音 6 = 57,一拍
notes.Add(new Tuple<byte, int>(, TEMPO));
// 中音 1 = 60,两拍
notes.Add(new Tuple<byte, int>(, * TEMPO));
// 中音 5 = 67,两拍
notes.Add(new Tuple<byte, int>(, * TEMPO));
// 中音 3 = 64,一拍
notes.Add(new Tuple<byte, int>(, TEMPO));
// 中音 1 = 60,一拍
notes.Add(new Tuple<byte, int>(, TEMPO));
// 低音 7 = 59,半拍
notes.Add(new Tuple<byte, int>(, TEMPO / ));
// 中音 2 = 62,半拍
notes.Add(new Tuple<byte, int>(, TEMPO / ));
// 低音 5 = 55,一拍
notes.Add(new Tuple<byte, int>(, TEMPO));
// 低音 7 = 59,一拍
notes.Add(new Tuple<byte, int>(, TEMPO));
// 中音 2 = 62,一拍
notes.Add(new Tuple<byte, int>(, TEMPO));
// 低音 7 = 59,一拍
notes.Add(new Tuple<byte, int>(, TEMPO));
// 低音 6 = 57,一拍
notes.Add(new Tuple<byte, int>(, TEMPO));
// 中音 1 = 60,两拍
notes.Add(new Tuple<byte, int>(, * TEMPO)); // 开始操作
foreach (var tp in notes)
{
// 开启音符
noteOn = new MidiNoteOnMessage(CHANNEL, tp.Item1, );
// 发送
mSynthesizer.SendMessage(noteOn);
// 延时
await Task.Delay(tp.Item2);
// 停止
noteOff = new MidiNoteOffMessage(CHANNEL, tp.Item1, );
// 发送
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 应用开发】MIDI 音乐合成——乐理篇

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    老周最近热衷于讲故事,接下来还是讲故事时间. 有人问我:你上大学的时候,有加入过学生会吗?读大学有没有必要加入学生会? 哎哟,这怎么回答呢,从短期来说,加入学生会有点用,至少可以娱乐一下,运气好的话, ...

随机推荐

  1. nova创建虚拟机源码分析系列之六 api入口create方法

    openstack 版本:Newton 注:博文图片采用了很多大牛博客图片,仅作为总结学习,非商用.该图全面的说明了nova创建虚机的过程,从逻辑的角度清晰的描述了前端请求创建虚拟机之后发生的一系列反 ...

  2. HTML5 设备上的API

    一.Vibration API ,接受两种类型参数 vibrate (unsigned long time)   当参数是unsigned long的时候 此时参数表示震动时间.  NotSuppor ...

  3. HTML学习笔记 cs2D3D展示基础 第十四节 (原创) 参考使用表

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. Nginx安装、平滑升级与虚拟机配置

    Nginx 高性能HTTP反向代理服务器,也是 LAMP/POP3/SMTP代理服务器 由内核和模块组成,内核通过找配置文件讲客户端请求映射到一个location(location是Nginx配置中的 ...

  5. 代码重构:用工厂+策略模式优化冗余的if else代码块

    最近在工作中优化了一段冗余的if else代码块,感觉对设计模式的理解和运用很有帮助,所以分享出来.鉴于原代码会涉及到公司的隐私,因此就不贴出来了.下面以更加通俗易懂的案例来解析. 假如写一个针对员工 ...

  6. Scrum Meeting Alpha - 2

    Scrum Meeting Alpha - 2 NewTeam 2017/10/25 地点:新主楼F座二楼 任务反馈 团队成员 完成任务 计划任务 安万贺 完成了大部分api的测试https://gi ...

  7. SSM 五:Spring核心概念

    第五章:Spring核心概念 一.Spring Ioc 优点: 1.低侵入式设计 2.独立于各种应用服务器 3.依赖注入特性将组建关系透明化,降低耦合度 4.面向切面编程的特性允许将通用性任务集中式处 ...

  8. MacOS 下安装mysqlclient 的问题及解决办法

    [操作环境] 操作系统:MacOS X 10.13.1 mysql运行环境:Docker Docker版本:17.09-ce 在开发Django时,刚开始使用的sqlite进行开发,想部署到生产环境需 ...

  9. ajax异步传送数据的方法

    1, 此方法为ajax异步发送后台数据的方法 var payment_id=$(this).attr("name"); alert(payment_id); $('.label') ...

  10. 从vultr购买到搭ss看世界

    title: 从Vultr购买到搭ss看世界 date: 2017-11-19 12:28:38 categories: 技术 tags: top: 800 password: 写在前面 ​ 服务器提 ...