上小学的时候,音乐课对于五音不全的我来说是痛苦的存在。好在老师并不好意思给一个勤奋听话的孩子不及格,所以我音乐课期末成绩通常都是60分。三年级的一天,老师宣布要期末考试,内容很简单,随机抽取教科书上的一个简谱唱出来即可。经过认真的思考,我认为这是一个能够攫取满分的机会。于是在考试前,我找了一个会唱的小伙伴,付出了几个大大泡泡糖的代价后,他教了我考试范围中的一首歌曲。回去后,我又在那首歌曲的简谱上用“多来米发 索 拉西”将每个音符重新标记,随后背了下来。在另一个阳光灿烂的音乐课上,同学们排队到老师面前抽签,我盯住前面同学抽中的这首歌曲,然后等到我之后以迅雷不及掩耳之势抽出来那张,在老师狐疑的目光中假装在看谱,然后背诵了出来…….如果说有时候一种声音,可以把人带回真实的过去。那么对于我来说,这个声音就是《小星星》-----那首二十多年前死记硬背下来的歌曲。 相比小号笛子之类的乐器,电子琴这样的键盘发生设备是更容易理解和操作的设备。如果将USB的键盘作为乐器按键,那么我们就能得到一个最简单的键盘乐器。为了达到这个目标,需要解决如下2个问题: 1. 发声的问题 2. USB键盘数据的解析; 首先需要一种能够发出乐器声音的模块。自然而然想到的是各种录音播放模块,但是很多时候会同时演奏出多个不同声音(可以理解为按下2个钢琴键实现同时发声)。对于这种情况下录音模块是无能为力的。接下来目光投向 目前已有的MIDI方案。MIDI是 MusicalInstrument Digital Interface(乐器数字接口)的简称。有时候中文会翻译成“迷笛”。这种技术是20世纪80 年代初为解决电声乐器之间的通信问题而提出的。简单的说这种方案规定了一种乐器通讯机制,比如当前按下了钢琴某个键,力度是多少。当音源设备收到这个消息后会根据内容播放实现准备好的声音。显而易见,这样的方案相比直接录音要节省大量空间,比如:十分钟左右的钢琴曲只有几十Kbytes的大小,WAV至少要十几Mbytes。经过比较,确定选用VS1053b 芯片的模块,这个芯片是单片OggVorbis/MP3/AAC/WMA/MIDI音频解码器,当它收到MIDI消息后会将乐器的声音直接发送出去。 选择的是微雪的 Music Shield 模块,是通过 SPI 接口和 Arduino 进行通讯。特别的我在阅读资料的时候特别看到,VS1053B 芯片支持一种实时Midi 模式,上电之后可以从RXPin 接受 MIDI 数据来发生。但是这种模式需要HW 设置VS1053B 引脚。市面上的模块并没有支持和预留。因此,这次的 Music Shield 仍然使用 SPI 接口,MIDI 数据也是从SPI 送到芯片中的。
对于第二个问题,很容易想到使用之前多次使用的USBHost Shield来完成。但是它存在占用 SPI 接口,代码复杂的缺点。经过比较最终确定使用的是南京沁恒微电子股份有限公司生产的USB键盘鼠标转串口通讯控制芯片 CH9350。南京沁恒是一家国产芯片公司,它设计了一些很有意思的USB 相关芯片,比如,市面上大量使用的廉价CH340 系列的USB转串口方案就是他们家的产品。这次使用的CH9350芯片是键盘鼠标远距离传输的方案。
例如,右侧的CH9350芯片解析操作者的USB键盘鼠标条码枪的数据,然后转换为TTL 串口信号(TXD,RXD)。这样可以用RS485或者网络发送到远端,再用另外的 CH3950芯片接收还原为一个USB设备从而实现远程操作的目的。同样的方法还可以实现一套键盘鼠标控制多台机器的目的。
硬件列表: 1. 1.CH9350 Mini 模块 2. 2.微雪MusicShield 模块 3. 3Arduino Proto Shield (用于固定CH9350 Mini,无任何元件) 4. 4.Arduino Proto Shield 扩展(用于插接Music Shield,无任何元件) 硬件连接(实际上我使用Shield,直接插上即可,并没有特别线路连接):
需要特别注意的是 CH9350模块上的 RSV 需要接地,这样会使得芯片工作在状态3,每次自动将解析后的键盘鼠标数据通过串口发出:
软件方面,首先因为选用的是 Arduino Uno,留下硬串口以便Debug,使用软串口来和 CH9350通讯。之后将键盘按键和MIDI 发生映射起来,使用的映射方法和Everyone Piano 相同:
代码:
- #include <SoftwareSerial.h>
- #include <SPI.h>
- #include <MusicPlayer.h>
-
- MusicPlayer player;
-
- //实例化软串口,连接到 Music Shield
- SoftwareSerial USBKB(2, 3); // RX, TX
-
- byte KeyToNote(byte key)
- {
- byte v=0;
- switch (key) {
- case 0x1D: //Z
- v=36;
- break;
- case 0x1B: //X
- v=38;
- break;
- case 0x06: //C
- v=40;
- break;
- case 0x19: //V
- v=41;
- break;
- case 0x05: //B
- v=43;
- break;
- case 0x11: //N
- v=45;
- break;
- case 0x10: //M
- v=47;
- break;
- case 0x36: //,
- v=48;
- break;
- case 0x37: //.
- v=50;
- break;
- case 0x38: // /
- v=52;
- break;
-
- case 0x04: //A
- v=48;
- break;
- case 0x16: //S
- v=50;
- break;
- case 0x07: //D
- v=52;
- break;
- case 0x09: //F
- v=53;
- break;
- case 0x0A: //G
- v=55;
- break;
- case 0x0B: //H
- v=57;
- break;
- case 0x0D: //J
- v=59;
- break;
- case 0x0E: //K
- v=60;
- break;
- case 0x0F: //L
- v=62;
- break;
- case 0x33: //;
- v=64;
- break;
- case 0x34: //'
- v=65;
- break;
-
- case 0x14: //Q
- v=60;
- break;
- case 0x1A: //W
- v=62;
- break;
- case 0x08: //E
- v=64;
- break;
- case 0x15: //R
- v=65;
- break;
- case 0x17: //T
- v=67;
- break;
- case 0x1C: //Y
- v=69;
- break;
- case 0x18: //U
- v=71;
- break;
- case 0x0C: //I
- v=72;
- break;
- case 0x12: //O
- v=74;
- break;
- case 0x13: //P
- v=76;
- break;
- case 0x2F: //[
- v=77;
- break;
- case 0x30: //]
- v=79;
- break;
-
- case 0x1E: //1
- v=72;
- break;
- case 0x1F: //2
- v=74;
- break;
- case 0x20: //3
- v=76;
- break;
- case 0x21: //4
- v=77;
- break;
- case 0x22: //5
- v=79;
- break;
- case 0x23: //6
- v=81;
- break;
- case 0x24: //7
- v=83;
- break;
- case 0x25: //8
- v=84;
- break;
- case 0x26: //9
- v=86;
- break;
- case 0x27: //0
- v=88;
- break;
- case 0x28: //-
- v=89;
- break;
- case 0x29: //=
- v=91;
- break;
- default:
- break;
- }
- return v;
- }
- void setup()
- {
- //用于Arduino Debug 串口
- Serial.begin(115200);
-
- //用于和CH3995通讯
- USBKB.begin(38400);
-
- //初始化 Midi 功能
- player.beginMidi();
-
- delay(2000);
-
- //设置主音量为最大
- player.midiWriteData(0xB0, 0x07, 127);
-
-
- //player.midiWriteData(0xB0, 0, 0x00); //Default bank GM1
-
- player.midiWriteData(0xC0, 1, 0);
- }
-
- //每笔 CH9350 键盘数据长度
- #define CH9350KBLENGTH 11
-
- //存放上一次 CH9350 发送过来的数据
- char Last[CH9350KBLENGTH]={0,0,0,0,0,0,0,0,0,0,0};
- //存放当前 CH9350 发送过来的数据
- char Current[CH9350KBLENGTH];
-
- byte i;
- boolean Found;
- void loop()
- {
- byte Index=0;
-
- //取得 CH9350 送过来的当前按键信息
- while (Index<CH9350KBLENGTH-1) {//不保存首位的 0x57 帧头,所以总共只有
- if ((USBKB.available())&&(0x57==USBKB.read())) {
- while (Index<CH9350KBLENGTH-1) {
- if (USBKB.available()) {
- Current[Index++]=USBKB.read();
- }
- }
- }
- }
-
- for (int p=0;p<CH9350KBLENGTH;p++) {Serial.print((byte)Current[p],HEX);Serial.print(" "); }
- Serial.print("\n");
-
- //检查 Current[Index],如果Last[]没有的,那么需要发送出去
- for (Index=4;Index<CH9350KBLENGTH;Index++) {
- Found=false;
- if (Current[Index]!=0) {
- for (i=4;i<CH9350KBLENGTH;i++) {
- if (Current[Index]==Last<i>) {
- Found=true;
- }
- }
- if (Found==false) {
- //Set instrument number. 0xC0 is a 1 data byte command
- player.midiNoteOn(0, KeyToNote(Current[Index]), 127);
- Serial.print("On ");
- Serial.println(Current[Index],HEX);
- Serial.print(" >> ");
- Serial.println(KeyToNote(Current[Index]));
- }
- }
- }
-
- //检查 Last[Index],如果Current[]里面没有,那么需要发送NoteOff命令
- for (Index=4;Index<CH9350KBLENGTH;Index++) {
- Found=false;
- if (Last[Index]!=0) {
- for (i=4;i<CH9350KBLENGTH;i++) {
- if (Last[Index]==Current<i>) {
- Found=true;
- }
- }
- if (Found==false) {
- player.midiNoteOff(0, KeyToNote(Current[Index]), 127);
- Serial.print("Off ");
- Serial.println(KeyToNote(Last[Index]),HEX);
- }
- }
- }
-
- for (Index=0;Index<10;Index++) {Last[Index]=Current[Index];}
- }</i></i>
复制代码
VS1053B 用来播放MIDI是很好的选择,这次使用的是微雪出品的Music Shield 价格在90左右,淘宝最低价格有35的,我觉得应该同样能够使用。
工作的视频
完整的代码和用的库
MusicShield.zip
代码
MusicKB.zip
|