Arduino 制作一个音乐键盘
userHead Zoologist 2020-09-30 10:13:25

上小学的时候,音乐课对于五音不全的我来说是痛苦的存在。好在老师并不好意思给一个勤奋听话的孩子不及格,所以我音乐课期末成绩通常都是 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 送到芯片中的。

projectImage

对于第二个问题,很容易想到使用之前多次使用的 USBHost Shield 来完成。但是它存在占用 SPI 接口,代码复杂的缺点。经过比较最终确定使用的是南京沁恒微电子股份有限公司生产的 USB 键盘鼠标转串口通讯控制芯片 CH9350。南京沁恒是一家国产芯片公司,它设计了一些很有意思的 USB 相关芯片,比如,市面上大量使用的廉价 CH340 系列的 USB 转串口方案就是他们家的产品。这次使用的 CH9350 芯片是键盘鼠标远距离传输的方案。

projectImage

例如,右侧的 CH9350 芯片解析操作者的 USB 键盘鼠标条码枪的数据,然后转换为 TTL 串口信号(TXD,RXD)。这样可以用 RS485 或者网络发送到远端,再用另外的 CH3950 芯片接收还原为一个 USB 设备从而实现远程操作的目的。同样的方法还可以实现一套键盘鼠标控制多台机器的目的。 

硬件清单
1x
CH9350 Mini 模块
1x
微雪 MusicShield 模块
1x
Arduino Proto Shield (用于固定 CH9350 Mini,无任何元件)
1x
Arduino Proto Shield 扩展(用于插接 Music Shield,无任何元件)

硬件连接(实际上我使用 Shield,直接插上即可,并没有特别线路连接):

projectImage

需要特别注意的是 CH9350 模块上的 RSV 需要接地,这样会使得芯片工作在状态 3,每次自动将解析后的键盘鼠标数据通过串口发出: 

projectImage

软件方面,首先因为选用的是 Arduino Uno,留下硬串口以便 Debug,使用软串口来和 CH9350 通讯。之后将键盘按键和 MIDI 发生映射起来,使用的映射方法和 Everyone Piano 相同:

projectImage
代码
#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];} 
}

VS1053B 用来播放 MIDI 是很好的选择,这次使用的是微雪出品的 Music Shield 价格在 90 左右,淘宝最低价格有 35 的,我觉得应该同样能够使用。 


工作的视频

联系我们
联系邮箱:makercarnival@hotmail.com
官方微信公众号:创客嘉年华
官方微博:上海创客嘉年华
© Copyright Zhiwei Robotics Corp. All Rights Reserved
code 上海创客嘉年华