【技术分享】单片机模拟NS手柄 半自动完成太鼓达人曲目

一、前言

1.1 项目由来

前些天,在b站上看到有人分享单片机模拟NS手柄,在《精灵宝可梦》、《异度之刃》等游戏中实现自动操作的视频。我是个有着多年“鼓龄”的太鼓达人玩家,于是产生想法,将该方案用于自动完成太鼓达人曲目,实现类似TAS的效果。经过一周的实验,取得了一定的成果:开发板通过USB TypeB转TypeC转接线连接至Switch游戏机的USB接口上,系统能够在操作者手动给出曲目开始信号(按下按键模块上的特定按键)的情况下,自动完成鬼难度6星的一首曲目,获得了“全良”的成绩,验证了方案的可行性。展示视频如下:

[自制] 单片机模拟Switch控制器_半自动完成太鼓达人曲目《恋》_20200224

备用链接

下面将简单介绍我的完成过程

1.2 前期调研工作

项目参考了以下链接中的内容:

“当单片机取代橡皮筋——解放双手,放飞双眼,我的宝可梦自动化成果”

“在 Switch上使用 Arduino Uno R3 开发板模拟连续 A 键”

此类项目基本是基于:https://github.com/progmem/Switch-Fightstick 二次开发而成。其中,单片机模拟USB功能的实现,主要依靠LUFA开源框架的使用,它可以让AVR单片机模拟成想要的USB设备。移植工作并不复杂,初始化函数无需自己修改;只需要自行编写业务代码即可。

1.3 测试曲目选择

测试曲目的选择方面:

  • 不能过于简单,否则达不到验证的目的。曲目在“鬼”难度中选择;
  • 为方便验证,简化编码工作,曲目速度不能过快或存在过多变化。最好是一个速度从头到尾;
  • 所选曲目不可过长,否则一方面会带来很多编码工作,另一方面也会受到单片机存储空间的限制。

综合考虑,选择了鬼难度的《恋》这首曲目。这首曲目是日剧《逃避可耻但有用》的片尾曲,官方难度为鬼6星,速度为BPM158,全曲不变。曲目基本涵盖了常用的节奏型,复杂度适中,适合验证使用。

二、项目实现

2.1 硬件平台

硬件平台为ARDUINO UNO R3开发板。特别要指出的是,此次开发中使用的是开发板上用作USB接口芯片的ATmega16U2(下图中红框圈出),并不是常规情况下用来开发的核心芯片ATMEGA328P。为避免干扰,减少功耗,可以将其从开发板上去除(下图中绿框圈出)。

通过研究硬件原理图可以发现,板子上的TX、RX两颗LED是连接在ATmega16上的,可供我们使用,项目中将用它来指示演奏音符的种类(鼓心、鼓边);同时,开发板上还提供了一些排针,连接至单片机的IO口上。我们将用来连接外部按键模块,用于输入。开发板原理图中ATMEGA16U2相关部分如下:

完成搭建的测试系统如图所示:

2.2 工具链及开发环境

单片机程序的编译环境为WinAVR-20100110,在windows和Linux下均可使用。为开发方便,本项目选择在Windows系统下进行开发。
本工程没有现成的集成开发环境(如Keil、Visual Studio等)可以使用,必须手写makefile,通过终端执行make指令进行编译。
于是,本项目的软件开发工作选择宇宙第一的文本编辑器——微软Visual Studio Code进行。不但代码编辑、makefile编辑非常方便,还自带终端,可以随时进行生成(make)、清理(clean)等操作,如下图。

项目进行make(生成)操作后,会生成用于执行的hex文件。下载hex文件使用Flip软件,通过USB线连接开发板和电脑,之后手动短接单片机的RESET引脚和GND(地)(如下图),待系统将设备枚举成功后,再进行下载,如下图。

2.3 软件实现

2.3.1 软件流程设计

拟定软件工作流程如下:

  1. 初始化(IO初始化、USB初始化等)。完成初始化后,循环执行以下 2 3 两项工作:
  2. 单片机持续检测按键模块上的SW1按键(连接至单片机PB2引脚),如果按下,则向Switch输出“A键按下”指令。即,用按键模块的SW1按键,模拟了Switch的A键。安排此功能,主要用于在菜单中进行曲目和难度的确认;以及演奏完毕的成绩确认。
  3. 单片机持续检测按键模块上的SW4按键(连接至单片机PB1引脚),如果按下,则立即调用“演奏”函数,按照程序的设计,向Switch有序发送按键信号,进行曲目的演奏。

接下来介绍软件实现过程中的几个关键点

2.3.2 音符时长标定
·音符和休止符

曲目的完成,实际上就是按照谱面演奏音符和休止符。具体到该项目,进行设计如下:

  • 音符的演奏为,输出相应的按键信号(鼓心“咚”音色输出B键、鼓边“咔”音色输出A键),之后,等待(延时)一定的时间,使得时值完整;
  • 休止符的演奏为,按照正确的时值,等待(延时)一定的时间。 为实现上述功能,我们首先要得到“一次输出动作”的最短时间。
·单次输出动作实验结果

我们在只使用B键用于输出“咚”音色,只使用A键用于输出“咔”音色的情况下,要完成一次完整、无误判的按键信号输出动作,需要进行以下步骤(以输出一次B键为例):

	HID_Task(B);
	USB_USBTask();
	_delay_ms(10);
	_delay_ms(10);
	
	HID_Task(PAUSE);
	USB_USBTask();
	_delay_ms(10);
	_delay_ms(10);

首先调用一次HID_Task(B)及USB_USBTask()函数进行输出,之后延时20毫秒;之后调用HID_Task(PAUSE)及 USB_USBTask()输出一个“PAUSE”信号,再延时20毫秒。总共需要40毫秒左右。

·使用逻辑分析仪进行音符时长标定

在明确了上述信息后,我们就可以对曲目中使用的音符、休止符进行标定了。曲目速度为BPM158,4/4拍。经过计算,得到如下结果:

  • 四分音符、休止符时长为379.7毫秒(一拍)
  • 八分音符、休止符时长为189.87毫秒
  • 十六分音符、休止符时长为93.9毫秒
  • 三十二分音符、休止符时长为47.46毫秒。曲目中的滚奏(“黄条”)暂定使用三十二分音符演奏

为减少编码量,节约存储空间,本项目中,使用毫秒作为最小计时单位。以演奏四分音符的“咚”音色为例,我们应当进行的操作如下:

  • 首先输出一次B键,约40毫秒;
  • 再延时379.7-40 ≈ 340毫秒。

演奏四分休止符时,应进行的操作如下:

  • 延时379.7-40 ≈ 340毫秒。

由于毫秒级延时精度有限,我们需要对实际的输出时间进行测量,以得到不同音符的误差,便于进行补偿。由于家中条件所限,没有示波器,我们使用逻辑分析仪进行标定工作,用以确定“演奏音符”操作中,完成按键输出后,需要延时的毫秒数;以及“演奏空拍”操作中,需要延时的毫秒数。完成连接的硬件如下图所示:

逻辑分析仪通过杜邦线,连接至单片机的IO口。测量时可以先将该IO口拉低,然后进行待测操作,之后再将IO口拉高。低电平的持续时间即为待测操作占用的时间。IO口翻转占用的时间为微秒级别,忽略不计。下图为测量上个章节提到的“单次按键输出”操作的结果图。从波形中可以看出,低电平(待测操作)持续时间为40.0733毫秒。

经过测试,我们可以得到各音符、休止符演奏时的实际时间,如下表所示(单位 毫秒)。我们根据测量结果,首先对部分延时的毫秒数进行修正,并记录下修正完毕依然残留的误差。

2.3.3 曲谱数据建立及演奏函数设计

进行如下设计:用16位整数(unsigned short int)组成的数组来描述曲目。
数字“1”表示演奏“咚”音色,数字2表示演奏“咔”音色,数字“5”表示曲目结束。其余数字则表示延时相应的毫秒数,如378表示将会调用_delay_ms()函数延时378毫秒。
为方便编码,设计宏定义如下:

//休止符
#define R4  (378) 
#define R8  (189)
#define R16 (95)
#define R32 (47)  

//演奏音符 1为演奏don 2为演奏ka 
#define PLAY_DON  1
#define PLAY_KA   2
#define HITDUR (40)

#define D4  PLAY_DON,(R4-HITDUR+1)  //修正
#define D8  PLAY_DON,(R8-HITDUR)
#define D16 PLAY_DON,(R16-HITDUR)
#define D32 PLAY_DON,(R32-HITDUR)

#define K4  PLAY_KA,(R4-HITDUR) 
#define K8  PLAY_KA,(R8-HITDUR)
#define K16 PLAY_KA,(R16-HITDUR)

曲目方面,图片格式的曲谱可以在太鼓达人wiki上获得,如图所示:

以前四小节为例,图中所示为:

在乐理上表示的意义为(谱例中普通符头为表示“咚” ×型符头表示“咔”,由于例程中没有给出左手上下左右按键的输出方法,所以大音符简化为同音色小音符处理):
按照前述宏定义,则前四小节编码为:
const unsigned short music[] PROGMEM = 
{
    //M1
    D4,     K4,     D4,     K4,         
    //M2
    D4,     K4,     D8, K16,K16,K8, D8,     
    //M3
    D4,     D4,     D8, K8, K8, D8,
    //M4
    R8, D4, D4,  K8,  K16,K16,K16,K16,

代码中为方便查看,以小节为单位分行编写。依次类推,完成整首曲目的编码。要注意的是,由于单片机的内存空间极其有限(仅512Byte),所以该数组不能像普通变量一样放在RAM中,而必须存放在FLASH中。数组定义时需要加入PROGMEM宏进行标志。

演奏函数设计为:在遇到结束标志之前,从数组中依次取出数字,判断数字并做出相应的动作。代码如下:

void play(void)
{
    unsigned short i = 0;
    while(1)
    {
      if(  pgm_read_word(&music[i]) == PLAY_DON)
      {
        PLAY_DON_B(); //通过B键演奏“咚”音色
      }
      else if(pgm_read_word(&music[i])== PLAY_KA )
      {
        PLAY_KA_A(); //通过A键演奏“咔”音色
      }
      else if (pgm_read_word(&music[i]) == MUS_END)
      {
        break;
      }
      else
      {
        _delay_ms(pgm_read_word(&music[i]));
      }

      i++;   

    }//while(1)
}//play()

为了访问FLASH中的数组数据,必须使用pgm_read_word()函数。相应的音色演奏函数如下(以PLAY_DON_B()函数为例):

void PLAY_DON_B(void)
{
  RXLED_ON;
  HID_Task(B);
  USB_USBTask();
  _delay_ms(10);
  _delay_ms(10);

  HID_Task(PAUSE);
  USB_USBTask();
  _delay_ms(10);
  _delay_ms(10);

  RXLED_OFF;
}

在演奏时,通过宏定义RXLED_ON、RXLED_OFF,操作IO口,使用了LED进行指示。演奏“咚”时,使用的是RXLED;相应的,在PLAY_KA_A()函数中,使用的是TXLED。

2.3.4 时间误差补偿

由于选择的最小时间单位为仅为毫秒,误差会随着演奏过程不断积累,导致后半段演奏出现问题。所以,应当统计各段落的误差,进行补偿。
曲目中各音符单独导致的误差是已知的,我们将各个片段的音符进行统计,计算总误差,再拟定各段落中补偿修正的毫秒数,将累计残余误差控制在1毫秒以内。如下表所示(单位 毫秒):

例如,前8小节中,经过统计,按照现有程序演奏完后,会比实际时间多出3.19毫秒。为了尽可能减少误差,就需要在8小节中平均的分配出用于补偿的-3毫秒。补偿时尽量选择时长较长的休止符。完成补偿的前八小节编码如下:
const unsigned short music[] PROGMEM = 
{
    //M1
    D4,     K4,     D4,     K4,         
    //M2
    D4,     K4,     D8, K16,K16,K8, D8,     
    //M3
    D4,     D4,     D8, K8, K8, D8,
    //M4
    R8, D4, D4,  K8,  K16,K16,K16,K16,

    //M5
    D4,    R4-2,         R8, D4, R8,
    //M6
    D4,     R8, D4,    R8,    K4,
    //M7
    D4,   R4-1,      R8, D4,     R8,
    //M8
    D4,   R8, D4,   R8, K4,

以此类推,完成整首曲目的误差修正。

2.3 实验验证

完成开发后进行验证实验。先在游戏的“曲目选择”中,将光标放到待选曲目上,连接开发板和Switch游戏机,待USB设备枚举完成后,按动按键模块上的SW1,代替Switch的A键,进行曲目和难度确定。进入曲目后,在合适的时机,手动按动SW4按键,启动输出流程。经过测试,只要首个音符能够抓出“良”,那么整首曲目基本可以保证全良。测试成绩如图所示:

理论上,只要单片机存储空间足够,使用该系统可以全良《太鼓达人Switch版》上任意一首曲目。

三、后记

后续可以改进的点(咕咕咕):

  • 描述曲目用的数据结构可以优化。为减少误差,可改成使用两个unsigned short int变量表示一次延时,分别用来表示毫秒级延时和微秒级延时,同时相对应的修改演奏函数。启用微秒级的延时,可大大减少误差,减小误差补偿方面的工作量。
  • 理论上可实现为全自动系统。将Switch的视频信号通过采集卡采集至上位机,上位机编写实时图像处理软件,用以检测第一个音符,在合适的时机通过串口向开发板发送开始信息,以代替人手操作。已有大神在《精灵宝可梦》中实现该技术方案。



项目完成后,我颇有些感慨。太鼓达人这款游戏,给予了我太多,改变了我太多。还记得上大学时,经常在街机厅忘我地练习,一有时间就和同好们愉快地交流、竞技。战胜过自己,也取得过成绩。甚至因为太鼓,开始与音乐结缘。它让我知道了自己的可能性,也让我了解了自己的天花板。曾几何时,也曾希望自己能够像传说中的大神们那样,鲜衣怒马,全良数百首曲目,功德圆满。怎奈天资不足,又没有条件保持一定强度的训练,只好安心当一名娱乐玩家。现在,通过自己完成的单片机系统,代替自己来实现全良的梦想,也算是对自己的一种交代。

欢迎交流


联系方式
gurn@tju.edu.cn
617442575@qq.com

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值