一、前言
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。爲避免干擾,減少功耗,可以將其從開發板上去除(下圖中綠框圈出)。
完成搭建的測試系統如圖所示:
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 軟件流程設計
擬定軟件工作流程如下:
- 初始化(IO初始化、USB初始化等)。完成初始化後,循環執行以下 2 3 兩項工作:
- 單片機持續檢測按鍵模塊上的SW1按鍵(連接至單片機PB2引腳),如果按下,則向Switch輸出“A鍵按下”指令。即,用按鍵模塊的SW1按鍵,模擬了Switch的A鍵。安排此功能,主要用於在菜單中進行曲目和難度的確認;以及演奏完畢的成績確認。
- 單片機持續檢測按鍵模塊上的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毫秒。
由於毫秒級延時精度有限,我們需要對實際的輸出時間進行測量,以得到不同音符的誤差,便於進行補償。由於家中條件所限,沒有示波器,我們使用邏輯分析儀進行標定工作,用以確定“演奏音符”操作中,完成按鍵輸出後,需要延時的毫秒數;以及“演奏空拍”操作中,需要延時的毫秒數。完成連接的硬件如下圖所示:
經過測試,我們可以得到各音符、休止符演奏時的實際時間,如下表所示(單位 毫秒)。我們根據測量結果,首先對部分延時的毫秒數進行修正,並記錄下修正完畢依然殘留的誤差。
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毫秒以內。如下表所示(單位 毫秒):
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按鍵,啓動輸出流程。經過測試,只要首個音符能夠抓出“良”,那麼整首曲目基本可以保證全良。測試成績如圖所示:
三、後記
後續可以改進的點(咕咕咕):
- 描述曲目用的數據結構可以優化。爲減少誤差,可改成使用兩個unsigned short int變量表示一次延時,分別用來表示毫秒級延時和微秒級延時,同時相對應的修改演奏函數。啓用微秒級的延時,可大大減少誤差,減小誤差補償方面的工作量。
- 理論上可實現爲全自動系統。將Switch的視頻信號通過採集卡採集至上位機,上位機編寫實時圖像處理軟件,用以檢測第一個音符,在合適的時機通過串口向開發板發送開始信息,以代替人手操作。已有大神在《精靈寶可夢》中實現該技術方案。
項目完成後,我頗有些感慨。太鼓達人這款遊戲,給予了我太多,改變了我太多。還記得上大學時,經常在街機廳忘我地練習,一有時間就和同好們愉快地交流、競技。戰勝過自己,也取得過成績。甚至因爲太鼓,開始與音樂結緣。它讓我知道了自己的可能性,也讓我瞭解了自己的天花板。曾幾何時,也曾希望自己能夠像傳說中的大神們那樣,鮮衣怒馬,全良數百首曲目,功德圓滿。怎奈天資不足,又沒有條件保持一定強度的訓練,只好安心當一名娛樂玩家。現在,通過自己完成的單片機系統,代替自己來實現全良的夢想,也算是對自己的一種交代。
歡迎交流
聯繫方式
[email protected]
[email protected]