2478浏览
查看: 2478|回复: 3

[ESP8266/ESP32] FireBeetle制作打击乐指示器

[复制链接]
凡用打、击方式发声的乐器(打弦乐器除外)统称为打击乐(器)。显而易见的是相对于弹奏和吹奏乐器,打击乐演奏更加简单。比如,著名的成语典故“渑池之”就记述了秦王现学现卖击缶的故事:“秦王派使臣告诉赵王,打算与赵王和好,在西河外渑池相会。赵王害怕秦王,想不去。廉颇、蔺相如商量说:“大王不去,显得赵国既软弱又怯懦。”赵王于是动身赴会,蔺相如随行。廉颇送到边境,跟赵王辞别时说:“大王这次出行,估计一路行程和会见的礼节完毕,直到回国,不会超过三十天。如果大王三十天没有回来,就请允许我立太子为王,以便断绝秦国要挟赵国的念头。”赵王同意廉颇的建议,就和秦王在渑池会见。秦王喝酒喝得高兴时说:“我私下听说赵王喜好音乐,请赵王弹弹瑟吧!”赵王就弹起瑟来。秦国的史官走上前来写道:“某年某月某日,秦王与赵王会盟饮酒,命令赵王弹瑟。”蔺相如走向前去说:“赵王私下听说秦王善于演奏秦地的乐曲,请允许我献盆缶给秦王,请秦王敲一敲,借此互相娱乐吧!”秦王发怒,不肯敲缶。在这时蔺相如走上前去献上一个瓦缶,趁势跪下请求秦王敲击。秦王不肯敲击瓦缶。蔺相如说:“如大王不肯敲缶,在五步距离内,我能够把自己颈项里的血溅在大王身上!”秦王身边的侍从要用刀杀蔺相如,蔺相如瞪着眼睛呵斥他们,他们都被吓退了。于是秦王很不高兴,为赵王敲了一下瓦缶。蔺相如回头召唤赵国史官写道:“某年某月某日,秦王为赵王击缶。”秦国的众大臣说:“请赵王用赵国的十五座城为秦王祝寿。”蔺相如也说:“请把秦国的都城咸阳送给赵王祝寿。”直到酒宴结束,秦王始终未能占赵国的上风。赵国又大量陈兵边境以防备秦国入侵,秦军也不敢轻举妄动。渑池会结束后,回到赵国,因为蔺相如功劳大,赵王任命他做上卿,位在廉颇之上。“【参考1
1999HanseulSoft公司制作过一款名为《VOS》(VirtualOrchestra Studio)的音乐游戏。其游戏操作界面是7个钢琴式的按键,当显示屏幕中的音符下落至游戏界面下方的准线时,玩家按下对应的按键,便可弹奏出该音符。这款叫做《VOS》的游戏,考验的是玩家对音乐节奏的把握以及手部反应能力。
FireBeetle制作打击乐指示器图1
铝板琴为法国米斯泰尔发明。最初的发音体用一系列音叉,亦称钢叉琴。1886年正式命名为钢片琴(又名铝板琴),实际的铝板琴音条和共鸣管均以铝制作。铝板琴外形如小形簧风琴,声源体为金属板条,以类似钢琴的击弦机击奏、有踏板制音器控制音响的长短,和键盘钢条琴相似,但每一钢音条下方附有共鸣管,放大音量,并使音色清晰纯净。钢片琴音域一般为C?C(4)2个半八度。【参考2
FireBeetle制作打击乐指示器图2

这次制作的目标是制作一款能够指示用户敲击铝板琴演奏音乐的设备。
硬件方面选择 DFRobot FireBeetleESP32 ,搭配WCH CH423来实现。设计的理念非常简单:FireBeetleESP32 通过串口和PC相连,后者发送控制信息,之后FireBeetleESP32 控制 CH423,再由CH423控制 15*8LED 提示用户操作。从上面也可以看出来,这个设计还可以修改为:通过无线发送数据进行控制,或者事先将要演奏的内容存储在 FireBeetle ESP32中,然后使用充电宝供电这样就彻底摆脱了电脑。

FireBeetle制作打击乐指示器图3

1.     为了布线方便,对 FireBeelte 进行了简化,只留下了必要的引脚;
2.     使用 CH423S封装芯片,关于这个芯片的使用可以在【参考3】看到;
3.     为了以后使用充电宝预留了一个电流消耗电路

FireBeetle制作打击乐指示器图4

这是LED 点阵 8*15LED
这个设计最大的挑战来自布线,因为PCB需要和铝板琴进行对应,因此尺寸上远超10*10cm,为了尽量降低成本,我们必须努力压缩面积(这也是为什么前面要把 FireBeetle ESP32 的符号修改的如此奇怪的原因)。
FireBeetle制作打击乐指示器图5
最终的尺寸是46.2*38.9cm,嘉立创打样价格是 60元。

FireBeetle制作打击乐指示器图6

FireBeetle制作打击乐指示器图7





首先编写Arduino 代码:

  1. #include <Wire.h>
  2. #define     LEDCOUNTER 15
  3. // CH423接口定义
  4. #define     CH423_I2C_ADDR1     0x20         // CH423的地址
  5. #define     CH423_I2C_MASK      0x3E         // CH423的高字节命令掩码
  6. #define CH423_SYSON1    0x0417    //开启自动扫描显示
  7. unsigned char CH423_buf[LEDCOUNTER];    //定义16个数码管的数据映象缓存区
  8. void CH423_Write( uint32_t cmd )    // 写命令
  9. {
  10.   //Serial.print("Address ");
  11.   //Serial.print(( unsigned char )(cmd >> 8), HEX);
  12.   //Serial.print("  command  ");
  13.   //Serial.print(( unsigned char ) (cmd & 0xff), HEX);
  14.   Wire.beginTransmission (( unsigned char )(cmd >> 8));
  15.   Wire.write( ( unsigned char ) (cmd & 0xff) );  // 发送数据
  16.   // 结束总线
  17.   if (Wire.endTransmission() == 0) {
  18.     //Serial.println(" I2C Success!");
  19.   } else {
  20.     //Serial.println("I2C error!");
  21.   }
  22. }
  23. // 向CH423输出数据或者操作命令,自动建立数据映象
  24. void CH423_buf_write( uint32_t cmd )
  25. {
  26.   if ( cmd & 0x1000 )
  27.   { // 加载数据的命令,需要备份数据到映象缓冲区
  28.     CH423_buf[ (unsigned char)( cmd >> 8 ) & 0x0F ] = (unsigned char)( cmd & 0xFF );    // 备份数据到相应的映象单元
  29.   }
  30.   CH423_Write( cmd );    // 发出
  31. }
  32. void setup() {
  33.   Serial.begin (115200);
  34.   Wire.begin (21, 22);   // sda= GPIO_21 /scl= GPIO_22.
  35.   /* INTENS [00-11]
  36.      OD_EN 使能开漏
  37.      X_INT 0x08
  38.      DEC_H 0x04
  39.      DEC_L 0x02
  40.      IO_OE 0x01
  41.   */
  42.   CH423_buf_write( 0x2417 );
  43.   /* OC_L_DAT  OC7-OC0 电平控制
  44.   */
  45.   CH423_buf_write( 0x2200 );
  46.   /* OC_H_DAT  OC15-OC8 电平控制
  47.   */
  48.   CH423_buf_write( 0x2300 );
  49.   // 初始化时保持全灭
  50.   uint32_t i;
  51.   for (i = 0; i < LEDCOUNTER; i++) {
  52.     CH423_buf_write(((0x30 + i) << 8) + 0x00);
  53.   }
  54. }
  55. byte Buf[LEDCOUNTER];
  56. byte Counter = 0;
  57. long int Elsp = 0;
  58. void loop() {
  59.   if (Serial.available() > 0) {
  60.     Buf[Counter] = Serial.read();
  61.     Counter++;
  62.     // 收到第一个开始计时
  63.     if (Counter == 1) {
  64.       Elsp = millis();
  65.     }
  66.     // 收到第16个就进行处理
  67.     if (Counter == LEDCOUNTER) {
  68.       for (int i=0;i<LEDCOUNTER;i++) {
  69.           Serial.print(Buf[i],HEX);
  70.           Serial.print("  ");
  71.           CH423_buf_write( ((0x30 + i) << 8) + Buf[i]);
  72.         }
  73.       Serial.println("");  
  74.       //重新计数
  75.       Counter = 0;
  76.       //计时器归零
  77.       Elsp = 0;
  78.     }
  79.   }
  80.   // 如果超过200ms没有收到,那么计数器归零
  81.   if ((Elsp!=0)&&(millis()-Elsp>200)) {
  82.       //重新计数
  83.       Counter = 0;
  84.       //计时器归零
  85.       Elsp = 0;
  86.     }
  87. }
复制代码

上位机会通过串口发送当前要点亮的第一个LED的位置,然后通过计时器移动点亮的LED,一直到最后的三个。
上位机代码是 c#编写,运行时会需要先打开乐谱文件。

FireBeetle制作打击乐指示器图8

主要代码如下:
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Data;
  5. using System.Drawing;
  6. using System.Linq;
  7. using System.Text;
  8. using System.IO;
  9. using System.Threading.Tasks;
  10. using System.Windows.Forms;
  11. using System.IO.Ports;    //串口控件
  12. using System.Threading;
  13. namespace CSI_MIPI_CLIENT
  14. {
  15.     public unsafe partial class Form1 : Form
  16.     {
  17.         public const int LEDCOUNTER=15;  // 共有15组 LED
  18.         public int NoteIndex = 0;
  19.         public int Elsp = 0;
  20.         // 发送给FireBeetle 的数据 Buffer
  21.         public byte[] Note = new byte[LEDCOUNTER];
  22.         // MIDI 数据对应LED 序列号
  23.         public int[] NoteToOrder = new int[LEDCOUNTER]
  24.                                 { 91,89,88,86,84,       // 5# 4# 3# 2# 1#
  25.                                   83,81,79,77,76,74,72, // 7 6 5 4 3 2 1
  26.                                   71,69,67              // #7 #6 #5
  27.         };
  28.         public const int ONEDELAY = 150;  // 一个延时150ms
  29.         // 记录音乐的结构体
  30.         public struct nodeStruct
  31.         {
  32.             public int time;        //绝对时间,比如: 1200
  33.             public int note;        //音符值, MIDI 的音符值
  34.             public int delayunit;   //延时数量, 比如:3, 表示从上一个到这个音符延迟3个单位
  35.         };
  36.         // 记录整首音乐
  37.         public List<nodeStruct> NoteList = new List<nodeStruct>();
  38.         public Form1()
  39.         {
  40.             InitializeComponent();
  41.         }
  42.         private void Form1_Load(object sender, EventArgs e)
  43.         {
  44.             if (System.IO.Ports.SerialPort.GetPortNames().Length != 0)
  45.             {
  46.                 // List COM all ports in comboxBox1
  47.                 comboBox1.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames());
  48.                 comboBox1.SelectedIndex = 0;
  49.                 serialPort1.BaudRate = 115200;
  50.                 serialPort1.DataBits = 8;
  51.                 serialPort1.Parity = 0;
  52.                 serialPort1.StopBits = (StopBits)1;
  53.                 serialPort1.Encoding = System.Text.Encoding.GetEncoding(28591);
  54.                 System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;
  55.                 // Show Application version in Form Title
  56.                 this.Text = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString() + "\n";
  57.             }
  58.             else
  59.             {
  60.                 textBox1.AppendText("[ERROR] No COM ports are found!");
  61.                 button1.Enabled = false;
  62.             }
  63.         }
  64.         // 打开串口
  65.         private void button1_Click(object sender, EventArgs e)
  66.         {
  67.             // 如果当前串口已经打开,那么就关闭之
  68.             if (serialPort1.IsOpen)
  69.             {
  70.                 //  停止定时器
  71.                 timer1.Enabled = false;
  72.                 // 更改按钮名称
  73.                 button1.Text = "Open Port";
  74.                 // 输出Log
  75.                 textBox1.AppendText("Close Port at " + DateTime.Now.ToString() + "\r\n");
  76.                 serialPort1.Close();
  77.             }
  78.             else
  79.             {
  80.                 // 如果串口未打开
  81.                 // 打开串口
  82.                 try
  83.                 {
  84.                     serialPort1.PortName = comboBox1.SelectedItem.ToString();
  85.                     serialPort1.Open();
  86.                 }
  87.                 catch (Exception Ex)
  88.                 {
  89.                     textBox1.AppendText("Open Port at" + comboBox1.SelectedItem.ToString() + " error \r\n");
  90.                     textBox1.AppendText(Ex.Message + " \r\n");
  91.                     return;
  92.                 }
  93.                 // Serial port is NOT opened
  94.                 button1.Text = "Close Port";
  95.                 textBox1.AppendText("Start at " + DateTime.Now.ToString() + "\r\n");
  96.             }
  97.         }
  98.         // 读取音乐文件
  99.         private void button2_Click_1(object sender, EventArgs e)
  100.         {
  101.             nodeStruct newValue = new nodeStruct();
  102.             if (openFileDialog1.ShowDialog() == DialogResult.OK)
  103.             {
  104.                 FileStream fs = new FileStream(openFileDialog1.FileName, FileMode.Open, FileAccess.Read);
  105.                 StreamReader sr = new StreamReader(fs);
  106.                 //使用StreamReader类来读取文件
  107.                 sr.BaseStream.Seek(0, SeekOrigin.Begin);
  108.                 // 从数据流中读取每一行,直到文件的最后一行
  109.                 string tmp = sr.ReadLine();
  110.                 int MiniDelay = 65535;
  111.                 while (tmp != null)
  112.                 {
  113.                     // 只有带有 Note_on_c 字样的才包含数据信息
  114.                     if (tmp.IndexOf("Note_on_c") > 0)
  115.                     {
  116.                         // 数据使用“,” 分割,这里拆解成字符串
  117.                         string[] arr = tmp.Split(',');
  118.                         // 只记录敲击发生的数据,不记录停止的数据
  119.                         if (Convert.ToInt32(arr[5]) != 0)
  120.                         {
  121.                             newValue.time = Convert.ToInt32(arr[1]);
  122.                             // 转化为 LED 的位置号
  123.                             for (int i = 0; i < LEDCOUNTER; i++) {
  124.                                 if (Convert.ToInt32(arr[4]) == NoteToOrder[i]) {
  125.                                     newValue.note = i;
  126.                                     break;
  127.                                 }
  128.                             }
  129.                            
  130.                             if (NoteList.Count > 0)
  131.                             {
  132.                                 MiniDelay = Math.Min(MiniDelay, newValue.time - NoteList[NoteList.Count - 1].time);
  133.                                 if (MiniDelay == 0) {
  134.                                     Thread.Sleep(100);
  135.                                 }
  136.                             }
  137.                             if (NoteList.Count == 0) {
  138.                                 newValue.delayunit = 0;
  139.                             }
  140.                             NoteList.Add(newValue);
  141.                         }
  142.                         
  143.                     }
  144.                     tmp = sr.ReadLine();
  145.                 }
  146.                 //关闭此StreamReader对象
  147.                 sr.Close();
  148.                 fs.Close();
  149.                 textBox1.AppendText("Total " + NoteList.Count.ToString() + "/Mininal delay " + MiniDelay.ToString() + "\r\n");
  150.                 // 给歌曲信息赋值间隔数据
  151.                 for (int i = 1; i < NoteList.Count; i++) {
  152.                     nodeStruct tmpNode = NoteList[i];
  153.                     tmpNode.delayunit = (NoteList[i].time - NoteList[i - 1].time) / MiniDelay;
  154.                     if (tmpNode.delayunit > 10) { tmpNode.delayunit = 9; }
  155.                     NoteList[i] = tmpNode;
  156.                 }
  157.                 for (int i = 0; i < NoteList.Count;i++) {
  158.                     textBox1.AppendText(i.ToString() + "  " +
  159.                                         NoteList[i].time.ToString() + "   "+
  160.                                         NoteList[i].note.ToString()+" "+
  161.                                         NoteList[i].delayunit.ToString()+"\r\n"
  162.                                         );
  163.                 }
  164.                     textBox1.AppendText("File loaded\r\n");
  165.                
  166.             }
  167.         }
  168.         private void timer1_Tick_1(object sender, EventArgs e)
  169.         {
  170.             // 处理最后几个下落
  171.             //LEDCOUNTER
  172.             for (int i = 0; i < LEDCOUNTER; i++)
  173.             {
  174.                 // 如果移动之前最低3Bits有1,那么清除之
  175.                 if ((Note[i] & 0b00000111) != 0)
  176.                 {
  177.                     Note[i] = (byte)(Note[i] & 0b11111000);
  178.                 }
  179.                 Note[i] = (byte)((Note[i] & 0xFF) >> 1);
  180.                 //如果移动之后Bit2为1,那么最后3位置起来
  181.                 if ((Note[i] & 0b00000100) != 0)
  182.                 {
  183.                     Note[i] = (byte)(Note[i] | 0b00000111);
  184.                 }
  185.             }
  186.             serialPort1.Write(Note, 0, LEDCOUNTER);
  187.             string s = NoteIndex.ToString()+" ";
  188.             for (int i = 0; i < LEDCOUNTER; i++)
  189.             {
  190.                 s = s + Note[i].ToString("X2") + "   ";
  191.             }
  192.             textBox1.AppendText(s + "\r\n");
  193.             // 如果当前已经是最后一个,那么就停止
  194.             if (NoteIndex == NoteList.Count) {
  195.                 int v = 0;
  196.                 for (int i = 0; i < LEDCOUNTER; i++) {
  197.                     v = v + Note[i];
  198.                 }
  199.                 if (v==0)
  200.                 {
  201.                     textBox1.AppendText("End of the song\r\n");
  202.                     timer1.Enabled = false;
  203.                     return;
  204.                 }
  205.                 return;
  206.             }
  207.             // 如果到达时间
  208.             if (Elsp >= NoteList[NoteIndex].delayunit)
  209.             {
  210.                 Note[NoteList[NoteIndex].note] = (byte)(Note[NoteList[NoteIndex].note]|0x80);
  211.                 serialPort1.Write(Note, 0, LEDCOUNTER);
  212.                 NoteIndex++;
  213.                 Elsp = 0;
  214.             }
  215.             else
  216.             {
  217.                 Elsp++;
  218.             }
  219.         }
  220.         private void button3_Click(object sender, EventArgs e)
  221.         {
  222.             if (timer1.Enabled)
  223.             {
  224.                 timer1.Enabled = false;
  225.                 button3.Text = "Start Send";
  226.             }
  227.             else
  228.             {
  229.                 for (int i = 0; i < LEDCOUNTER; i++) {
  230.                     Note[i] = 0xFF;
  231.                     serialPort1.Write(Note, 0, LEDCOUNTER);
  232.                     Thread.Sleep(100);
  233.                 }
  234.                 Thread.Sleep(200);
  235.                 for (int i = LEDCOUNTER-1; i >-1 ; i--)
  236.                 {
  237.                     Note[i] = 0x00;
  238.                     serialPort1.Write(Note, 0, LEDCOUNTER);
  239.                     Thread.Sleep(100);
  240.                 }
  241.                 button3.Text = "Stop Send";
  242.                 timer1.Interval = ONEDELAY;
  243.                 timer1.Enabled = true;
  244.                 NoteIndex = 0;
  245.                 Elsp = 0;
  246.             }
  247.         }
  248.     }
  249. }
复制代码



另外还有一个关键的问题:如何制作软件使用的数据。我是用的是midicsv这个软件,它能够提取MID文件的数据信心保存在 csv文件中,这个软件的使用方法在之前的文章中有介绍过【参考3】。

参考:




ThuApril-202304139568..png

zoologist  高级技匠
 楼主|

发表于 2023-4-13 15:26:03

本文提到的应用程序代码:
下载附件Note2Com.zip

本文提到的 midi 提取工具
下载附件note文件制作文件.zip
回复

使用道具 举报

zoologist  高级技匠
 楼主|

发表于 2023-4-13 15:27:31


工作的测试视频
回复

使用道具 举报

若晗  中级技师

发表于 2023-5-4 14:34:04

学习了,感谢分享
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4

© 2013-2024 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail