用C/C++寫(xiě)一個(gè)簡(jiǎn)易的鋼琴小程序

0.緣由

C語(yǔ)言課期末大作業(yè)。由于是開(kāi)放性的作業(yè),隨便寫(xiě)著玩的,就寫(xiě)了這么一個(gè)玩意。雖然高中的時(shí)候接觸過(guò)一些音樂(lè)或者音頻軟件,像Au,F(xiàn)Lstudio,Minecraft之類的,但實(shí)際上對(duì)音樂(lè)方面的還算是一竅不通,其原因是不同于許多同齡人,在小的時(shí)候并沒(méi)有被父母逼著去學(xué)一門(mén)樂(lè)器,也就沒(méi)有接受過(guò)正規(guī)的樂(lè)理方面的教育。如果本文或文中代碼里出現(xiàn)一些低級(jí)的音樂(lè)常識(shí)方面的錯(cuò)誤,還請(qǐng)不吝賜教。當(dāng)然,作為半年前剛剛接觸代碼的大一新生,代碼部分存在很多漏洞,不規(guī)范之處和可優(yōu)化的地方,也希望能夠給予諒解并多多指教。

1.環(huán)境

Win10系統(tǒng)

Micosoft cl編譯器,msvc開(kāi)發(fā)者工具包

VS2019白嫖版附贈(zèng)的cl編譯器,為什么用這個(gè)后文會(huì)講

Visual Studio Code編輯器,coderunner插件,C/C++拓展插件

vscode里的tasks.json文件。不要問(wèn)我任務(wù)名為什么既是build又是test,問(wèn)就是腦子抽了
vscode里的launch.json文件,此次不需要用到下面那個(gè)g++

引用頭文件或類:iostream, thread, string.h, windows.h, conio.h, mmsystem.h, stdlib.h

鏈接的庫(kù):winmm.lib

計(jì)算機(jī)配置:

家貧,買(mǎi)不起好機(jī)子

2.思路

思考編寫(xiě)過(guò)程中可能會(huì)出現(xiàn)的問(wèn)題和需要特別關(guān)照的點(diǎn):

2.1如何在程序內(nèi)播放聲音?

上網(wǎng)查了一下,似乎是在mmsystem.h頭文件中,提供了一個(gè)windows本身的api函數(shù)mciSendString,可以播放媒體文件。但不幸的是,使用這個(gè)函數(shù)需要鏈接一個(gè)動(dòng)態(tài)庫(kù),而我一直以來(lái)使用的g++編譯器不僅鏈接起來(lái)十分麻煩,g++自帶的那些庫(kù)中還找不到這個(gè)庫(kù)。many shoes?查了一下,大概是說(shuō)gcc和g++屬于linux系的編譯器,因此其提供的很多api函數(shù)都是對(duì)接linux的。無(wú)奈,只好下載了微軟家的VS2019白嫖版(理論上來(lái)說(shuō)visual c++之流應(yīng)該也可以)。在調(diào)整了一些環(huán)境變量之后,用cl編譯了一次,發(fā)現(xiàn)可以播放聲音了。

2.2鋼琴上每個(gè)鍵的聲音從哪來(lái)?

這個(gè)算是一個(gè)比較簡(jiǎn)單的問(wèn)題,觀察一下網(wǎng)上一些在線鋼琴:

圖片原網(wǎng)址:https://virtualpiano.net/

發(fā)現(xiàn)在打開(kāi)網(wǎng)頁(yè)的時(shí)候服務(wù)器會(huì)發(fā)送一系列MP3文件過(guò)來(lái)。不用說(shuō),就是每個(gè)鍵對(duì)應(yīng)的音。那么接下來(lái)就好辦了,遂設(shè)計(jì)爬蟲(chóng)爬取之:

import requests
jianlist=['C','Cs','D',"Ds","E","F","Fs","G","Gs","A","As","B"]
jielist=[str(x) for x in range(7)] 
for jian in jianlist:
    for jie in jielist:
        url = "https://cdn.jsdelivr.net/gh/warpprism/cdn@latest/au\
            topiano/static/samples/bright_piano/"+jian+jie+".mp3"
        path = 你所需要的路徑+jian+jie+".mp3"
        headers = {
            "User-Agent":"Mozilla/5.0 (Windows NT 6.3; Win64; x64) Apple\
                  WebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"
        }
        respones = requests.get(url,headers=headers)
        with open(path,mode="wb") as f:
            f.write(respones.content)
        print(path+" done")
print("end")

當(dāng)時(shí)寫(xiě)這個(gè)爬蟲(chóng)是用來(lái)爬https://www.autopiano.cn/這個(gè)網(wǎng)站的(非常感謝autopiano.cn對(duì)本程序編寫(xiě)過(guò)程中提供的借鑒和啟發(fā)意義,以及非常抱歉用爬蟲(chóng)消耗了一部分服務(wù)器資源,真心非常抱歉),但截至發(fā)稿時(shí)間該網(wǎng)站已更換了底層源碼(應(yīng)該是叫這個(gè)吧?),打開(kāi)網(wǎng)頁(yè)和更換音色的時(shí)候服務(wù)器不會(huì)再發(fā)送一系列的MP3文件過(guò)來(lái)了,而是會(huì)發(fā)送一個(gè)js文件。具體的工作原理我也不是很懂,有待以后進(jìn)一步探究。

2.3如何實(shí)現(xiàn)短時(shí)間內(nèi)播放多個(gè)聲音?

鋼琴上一個(gè)音可以持續(xù)3秒,甚至更多。但兩個(gè)音之間的間隔遠(yuǎn)遠(yuǎn)小于3秒。如何在使用mciSendString函數(shù)播放聲音的同時(shí),使程序不停止在該函數(shù)處,而是繼續(xù)運(yùn)行并播放下一個(gè)音?
答案是多線程。
但由于將之前的g++編譯器換成了cl編譯器,而微軟的開(kāi)發(fā)者工具包中沒(méi)有pthread.h,手動(dòng)引入pthread.h又比較復(fù)雜(太懶),所以本程序使用的是c++11引入的、功能遠(yuǎn)沒(méi)有pthread.h豐富的、但是是由微軟的開(kāi)發(fā)者工具包中自帶的、使用起來(lái)非常簡(jiǎn)便的、與windows系統(tǒng)有天然適應(yīng)性(大概)的thread類。
(好像有個(gè)process.h也能多線程編程?但是用起來(lái)有點(diǎn)麻煩。本程序涉及的多線程方面的東西都比較淺顯,所以是越簡(jiǎn)單越好)

2.4如何實(shí)現(xiàn)鍵盤(pán)按鍵與聲音的對(duì)應(yīng)?

由于爬下來(lái)的聲音文件名是對(duì)應(yīng)的音名(當(dāng)時(shí)是這樣的),肯定不能直接輸入音名來(lái)播放。方法之一是將每個(gè)音的文件名改成對(duì)應(yīng)按鍵的名字,但這樣手動(dòng)操作量較大。我的想法是使用一個(gè)decode函數(shù)進(jìn)行解碼(?),通過(guò)一定的規(guī)律將按鍵映射成相應(yīng)的音名。

2.5如何實(shí)現(xiàn)按下對(duì)應(yīng)鍵后立馬播放對(duì)應(yīng)的聲音?

平常使用的getchar函數(shù)、scanf函數(shù)以及cin方法等都需要按下回車后才能被程序所接收,這些函數(shù)顯然不符合需求。經(jīng)過(guò)網(wǎng)上查詢后得知,conio.h中的getch函數(shù)和_getch函數(shù)有這樣的效果。

2.6如何實(shí)現(xiàn)不斷地接收輸入并播放對(duì)應(yīng)聲音?

死循環(huán),while(True).....

2.7如何實(shí)現(xiàn)自動(dòng)按輸入的簡(jiǎn)譜演奏?

可以設(shè)置將簡(jiǎn)譜寫(xiě)在一個(gè)文件中,播放時(shí)讀取這個(gè)文件。原本的想法是直接將簡(jiǎn)譜的數(shù)字寫(xiě)在文件中,用*代表升8度,用.代表降8度,音符之間以空格間隔,一次用%s讀取一個(gè)音符,然后再通過(guò)另一個(gè)decode函數(shù)解碼為對(duì)應(yīng)音名。但最終發(fā)現(xiàn)簡(jiǎn)譜實(shí)在過(guò)于復(fù)雜,應(yīng)由人工將簡(jiǎn)譜上的音符轉(zhuǎn)換為應(yīng)該按下的按鍵,再直接將按鍵輸入文件中,相當(dāng)于是將stdin重定向?yàn)槟骋晃募髦蟮氖謩?dòng)模式。

2.8如何存儲(chǔ)人肉解碼好的譜子?

如圖:


圖為在vscode中打開(kāi)的樣子(樂(lè)曲為平凡之路)

對(duì)照一下原樂(lè)譜:


來(lái)源:http://www.doc88.com/p-7794828256780.html

3.結(jié)構(gòu)

首先這個(gè)程序初步確定有兩個(gè)模式:自動(dòng)和手動(dòng)。兩個(gè)模式的界面應(yīng)該是不同的。所以根據(jù)結(jié)構(gòu)化編程思想,一個(gè)函數(shù)負(fù)責(zé)一個(gè)模式,分別是automode()和manualmode()。既然有兩個(gè)模式,那么肯定要有一個(gè)選擇模式的開(kāi)始界面,設(shè)為beginmode()。又開(kāi)始必有結(jié)束,還要有一個(gè)endmode()。Mode之間的跳轉(zhuǎn)可以放在main里,用一個(gè)int來(lái)存放跳到哪一個(gè)mode的信息。
所以main的偽代碼大概為:(注意接下來(lái)都是偽代碼

Int main(){
    Int mode=0;
    While(true):
        If mode==0:
            Mode = Beginmode()
        If mode==1:
            Mode = manualmode()
        If mode==2:
            Mode = automode()
        If mode==3:
            Mode = endmode()
            Break //這個(gè)地方好像用switch語(yǔ)句也可以
    System.cls;
    Return 0;
}

而beginmode應(yīng)該是這樣:

Int beginmode(){
    Cout << 一些開(kāi)始的字 << endl;
    Cout << 選擇你的模式:1.手動(dòng) 2.自動(dòng) 3.退出 << endl;
    Int 選擇=0;
    Scanf(“%d”,&選擇);
    檢測(cè)輸入,不符合的重新輸入;
    Sys.cls;
    Return 選擇;
}

所以endmode:

Int endmode(){
    Cout << 一些信息,代表程序結(jié)束了 << endl;
    System.pause;   //讓用戶看一下那些字
    Return 0;
}

而自動(dòng)擋和手動(dòng)擋比較復(fù)雜,應(yīng)該更加細(xì)分的來(lái)討論。
手動(dòng)擋的話,首先最核心的應(yīng)該是有一個(gè)播放聲音的函數(shù)playsound(char)和decode()函數(shù),這里我選擇將decode()函數(shù)放在playsound里面,原因后面會(huì)講。然后就是多線程。所以應(yīng)該這么寫(xiě):

Int manualmode(){
    Print(一些信息);
    聲明一些變量;
While(true):
        字符變量 = getch();
        If(是回車):
            Break;
        創(chuàng)建線程并運(yùn)行playsound;/*此處傳入變量較為麻煩,因此直接
        將字符變量傳入,使解碼在playsound中進(jìn)行*/
        分離線程;//不分離的話程序會(huì)阻塞于此處,直至音頻播放完畢
    Print(退出);
    Sys.cls;
    Return 0;
}

自動(dòng)擋的話,實(shí)際上是在手動(dòng)擋的基礎(chǔ)上改進(jìn)的,因此實(shí)際上也差不多:

Int automode(){
    FILE *f;
    選擇打開(kāi)的文件;
    打開(kāi)文件流;
    While(true):
        字符變量 = fgetc();
        Print(字符變量);
        If 字符變量是一些譜子上用于標(biāo)記的,與演奏無(wú)關(guān)的字符:
            Continue;
        創(chuàng)建線程并運(yùn)行playsound;
        分離線程;
        Sleep(一定秒數(shù),根據(jù)曲速而定);
    Print(一些信息,告訴用戶結(jié)束了);
    Sys.pause;
    Sys.cls;
    Return 0;
}

其余的函數(shù)較為細(xì)節(jié),這里就不寫(xiě)了。

4.實(shí)現(xiàn)

略,理由是找不到當(dāng)初的代碼了。。。

5.分析

將所有模塊們串聯(lián)起來(lái)后,就有了一個(gè)界面不怎么精致,輸入不怎么安全,但基本功能已經(jīng)齊全的初版程序。當(dāng)然,存在很多問(wèn)題和可該進(jìn)之處:

5.1按鍵的自動(dòng)重復(fù)問(wèn)題。

用過(guò)電腦的人都知道,按住一個(gè)鍵會(huì)打出來(lái)一連串的字符。在此程序中,表現(xiàn)為:按住一個(gè)鍵會(huì)連續(xù)觸發(fā)多次播放。而現(xiàn)實(shí)中的鋼琴顯然不會(huì)發(fā)生這種情況,網(wǎng)頁(yè)上的在線鋼琴也不會(huì)。網(wǎng)上針對(duì)類似問(wèn)題給出的解決方案是調(diào)用一個(gè)windows的關(guān)于鍵盤(pán)的api函數(shù),但使用這個(gè)方案會(huì)使得手動(dòng)模式下失去兩鍵同時(shí)按下同時(shí)發(fā)音的能力,原因是其調(diào)用速度較慢,兩次循環(huán)之間相當(dāng)于sleep了一小段時(shí)間。經(jīng)過(guò)深思熟慮之后,我發(fā)現(xiàn)按住鍵盤(pán)的問(wèn)題可以通過(guò)彈琴的人來(lái)解決(“如果不能解決問(wèn)題,就解決提出問(wèn)題的人”的思想),而不能同時(shí)彈兩個(gè)音,對(duì)于任意一個(gè)會(huì)彈鋼琴的人來(lái)說(shuō),是不可容忍的。在兩種情況中比較之后,我選擇了不解決這個(gè)問(wèn)題(待有緣人來(lái)解決這個(gè)問(wèn)題)。

5.2音頻播放時(shí)的第一個(gè)音頻的延遲播放問(wèn)題。

無(wú)論是自動(dòng)擋還是手動(dòng)擋,在播放第一個(gè)音的時(shí)候,都會(huì)停頓一小會(huì),然后再播放,這樣的話跟第二個(gè)音的間隔時(shí)間會(huì)很短很短,甚至重疊。在查看調(diào)試控制臺(tái)的運(yùn)行記錄后,發(fā)現(xiàn)之前很久沒(méi)有播放音頻的第一次播放音頻時(shí),系統(tǒng)會(huì)加載一大堆相關(guān).dll文件,而一段時(shí)間不播放音頻后,系統(tǒng)會(huì)自動(dòng)unload這些.dll文件。解決這個(gè)問(wèn)題的方法之一是手動(dòng)來(lái)讓系統(tǒng)load/unload這些.dll文件,但缺點(diǎn)是比較復(fù)雜。所以我選擇了一個(gè)比較愚蠢的辦法:在自動(dòng)擋和手動(dòng)擋最后一次用戶輸入之后,播放一個(gè)25秒無(wú)聲的MP3文件來(lái)強(qiáng)制加載相關(guān)dll,優(yōu)點(diǎn)是操作簡(jiǎn)單,缺點(diǎn)是手動(dòng)模式下閑置時(shí)間過(guò)長(zhǎng)時(shí),再次開(kāi)始彈時(shí)還是會(huì)出現(xiàn)第一個(gè)音的延時(shí)問(wèn)題,而且看起來(lái)很蠢,并沒(méi)有從根本上解決問(wèn)題。

5.3各個(gè)需要用戶輸入的地方的輸入檢測(cè)問(wèn)題。

初版的程序?qū)τ谟脩舻脑O(shè)想過(guò)于理想,未考慮用戶不按規(guī)則輸入的情況。解決方法開(kāi)始時(shí)想的是用fflush(stdin),但發(fā)現(xiàn)好像不管用,輸出一下發(fā)現(xiàn)是清除成功了的,但不知為何就是不行,最后還是用了while加getchar才解決。

5.4自動(dòng)擋下短時(shí)間播放大量音頻時(shí)曲速變慢問(wèn)題。

原因應(yīng)該是因?yàn)殚_(kāi)的線程太多了,資源占用太大。但是要減少線程的話肯定是不行的,畢竟曲子一定要聽(tīng)完整的。所以我建議的解決方法是換一臺(tái)機(jī)能更強(qiáng)大的電腦,但苦于資金有限而無(wú)法施行。

5.5曲速需要手動(dòng)輸入的問(wèn)題。

眾所周知每個(gè)曲子的曲速不盡相同,但曲譜文件中只有譜子這一信息,這樣一來(lái)就必須手動(dòng)輸入曲速,這無(wú)疑會(huì)給不知道曲速的用戶操作帶來(lái)極大的不便。解決方法是在開(kāi)頭處整一個(gè)信息頭,包含了曲名,曲速,版本等信息。

5.6自動(dòng)播放途中無(wú)法操作的問(wèn)題。

有時(shí)候聽(tīng)了一半不想聽(tīng)了,卻不能退出,這樣的設(shè)計(jì)實(shí)在是不人性化。解決方案是利用conio.h中的kbhit()函數(shù)檢測(cè)有無(wú)鍵按下,沒(méi)有的話不進(jìn)入if分支防止曲速因此拖慢,有的話檢測(cè)是不是回車,是的話直接跳出播放的循環(huán)。

當(dāng)然,還有其他各種小問(wèn)題,由于過(guò)于細(xì)節(jié)故此處不列出。

6.打磨

前面提到過(guò),初版的界面極其簡(jiǎn)陋,雖然不要求什么高大上的UI設(shè)計(jì),但至少需要一個(gè)用戶可以看懂的界面。所以:

6.1給整個(gè)程序畫(huà)一個(gè)框框。

讓內(nèi)容都在框框里顯現(xiàn),使其更像一個(gè)真正的游戲。當(dāng)然這其實(shí)不算是一個(gè)小功能,或者說(shuō),開(kāi)始時(shí)我覺(jué)得這似乎是一個(gè)小功能,原因是這應(yīng)該算一個(gè)大功能。如何讓內(nèi)容在框框中顯現(xiàn),如何消除框框內(nèi)的內(nèi)容,如何排版,如何在調(diào)節(jié)顯示的同時(shí)不影響同時(shí)正在演奏的音樂(lè)等等一個(gè)個(gè)問(wèn)題都可以說(shuō)是非常復(fù)雜了。

6.2在手動(dòng)模式下畫(huà)一個(gè)鋼琴鍵盤(pán)圖。

就是照著網(wǎng)頁(yè)上的用字符畫(huà)一個(gè)鍵盤(pán),有什么難的?我原本是這么想的,但奈何這個(gè)鍵盤(pán)的圖案實(shí)在是太復(fù)雜了,根本無(wú)法找出合適的規(guī)律來(lái)用循環(huán)打印出來(lái),而網(wǎng)絡(luò)上所教的一些方法(對(duì)于我來(lái)說(shuō))又過(guò)于復(fù)雜。最終只能非常愚蠢地直接將畫(huà)好的圖案打印上去。

6.3將曲速與曲子捆綁,保存在文件中。

上面已經(jīng)提到過(guò),具體是用一個(gè)結(jié)構(gòu)體來(lái)直接保存信息頭的所有信息。

6.4將目錄下所有的曲子顯示出來(lái)以供用戶選擇。

用system(“dir 路徑”)可以方便地查看路徑下的所有文件,在其中篩選.dat文件打印在屏幕上并標(biāo)序號(hào),然后添加入聲明好的字符串?dāng)?shù)組中,隨后便可通過(guò)序號(hào)-1作為字符串?dāng)?shù)組的行下標(biāo)來(lái)選擇要播放的曲子。

6.5結(jié)尾處整一個(gè)制作人員名單。

單純地打印出來(lái)的話其實(shí)挺簡(jiǎn)單,難就難在我希望能夠像游戲或電影那樣整一個(gè)滾動(dòng)的字幕。這樣一來(lái)可以使名單的長(zhǎng)度不受框框大小限制,二來(lái)比較正式。最終實(shí)現(xiàn)方案是用字符串?dāng)?shù)組加上兩層for循環(huán)。

6.6實(shí)現(xiàn)雙音軌播放。

眾所周知鋼琴是用兩只手彈的,但初版的自動(dòng)擋只能在同一時(shí)間播放一個(gè)音,這樣的話雖然也是能夠演奏音樂(lè),但實(shí)在是過(guò)于單薄,無(wú)法復(fù)現(xiàn)一些比較復(fù)雜的曲子。解決方法是將原先的自動(dòng)擋的循環(huán)里面的大部分內(nèi)容都提取出來(lái),整合成一個(gè)函數(shù)playsong(),再額外寫(xiě)一個(gè)和弦的譜子,然后額外整一個(gè)文件流,然后每次循環(huán)開(kāi)兩個(gè)線程來(lái)播放兩個(gè)音,并且將主線程調(diào)整為合并模式,即不運(yùn)行完主線程不進(jìn)行下一步操作,而另一個(gè)線程則設(shè)為分離模式。這樣的話,每一次循環(huán)兩個(gè)線程都相當(dāng)于進(jìn)行了一次強(qiáng)行同步,避免了單獨(dú)開(kāi)兩個(gè)循環(huán)可能造成的兩個(gè)音軌不同步的情況。

中途也遇到了一些其它的小問(wèn)題,但過(guò)于細(xì)節(jié)此處不予以列出。

7.成品

雙擊exe文件,可打開(kāi)一個(gè)初始界面:


初始界面,有點(diǎn)簡(jiǎn)陋

輸入1,按回車后進(jìn)入手動(dòng)模式:


手動(dòng)擋,畫(huà)面是靜態(tài)的,十分簡(jiǎn)陋

此時(shí)可以根據(jù)對(duì)應(yīng)音名的按鍵來(lái)彈鋼琴了。
按回車可以返回主頁(yè)面。然后輸入2后按回車可以進(jìn)入播放界面:


播放界面,樂(lè)曲還比較少

此時(shí)可以選擇需要播放的音樂(lè)。(注:single_ver為單音軌版)此處我們選擇9號(hào)音樂(lè)《平凡之路》:


播放中....

可以看見(jiàn)此時(shí)程序正在自動(dòng)播放音樂(lè)。此時(shí)按下回車可以直接跳到音樂(lè)播放完的那一步:


放完了,按任意鍵回到主頁(yè)面

按下任意鍵后可以回到主頁(yè)面。輸入3按回車可以觀看制作人員名單。
看完后按任意鍵可退出程序:(播放中途按任意鍵可加速播放)


語(yǔ)出自——頭號(hào)玩家,哈利迪

8.反思

雖然程序是寫(xiě)了出來(lái),而且能夠良好運(yùn)行,但與預(yù)期還是有較大差別:

  1. 未能實(shí)現(xiàn)像網(wǎng)頁(yè)上的那些鋼琴一樣,手動(dòng)模式下按下一個(gè)鍵時(shí)對(duì)應(yīng)圖形會(huì)發(fā)生變化,以表示按下。
  2. 未能實(shí)現(xiàn)像網(wǎng)頁(yè)上的一樣,自動(dòng)播放時(shí)有一個(gè)音符雨的效果。且自動(dòng)播放時(shí)只能在屏幕上顯示主音軌的對(duì)應(yīng)按鍵,無(wú)法顯示另一個(gè)音軌的按鍵,不利于用戶學(xué)習(xí)。
  3. 對(duì)于音軌的可拓展性較弱。雙音軌對(duì)于鋼琴來(lái)說(shuō)是遠(yuǎn)遠(yuǎn)不夠的,現(xiàn)實(shí)中的鋼琴有的時(shí)候甚至?xí)瑫r(shí)產(chǎn)生五六個(gè)音。
  4. 播放音頻較為密集的時(shí)候會(huì)出現(xiàn)卡頓的情況,但此時(shí)CPU還是非常的空閑,可能是出現(xiàn)了所謂的“一核有難,七核圍觀”現(xiàn)象。如何充分的調(diào)動(dòng)CPU分擔(dān)計(jì)算任務(wù),是一個(gè)非常值得思考的問(wèn)題。
  5. 受電腦鍵盤(pán)所限,音域較為狹窄,也讓熟悉鋼琴的人彈起來(lái)不怎么習(xí)慣。
  6. 未能實(shí)現(xiàn)圖形化界面,并不能稱得上是一款真正的游戲。

因此,如果要改進(jìn)的話,我認(rèn)為可以從以下幾個(gè)方面入手:

  1. 增加對(duì)midi文件的支持。當(dāng)前計(jì)算機(jī)界已有一種主流的記譜文件格式,即midi。Midi文件中詳細(xì)地記錄了一支曲子中每個(gè)音的音高,出現(xiàn)的時(shí)間點(diǎn),持續(xù)時(shí)間,聲音大小,音色等信息。很明顯,比我這個(gè)自己發(fā)明的記譜法不知道高到哪里去了。引入對(duì)midi文件的支持,不僅解決了自動(dòng)模式中的音軌拓展性差問(wèn)題,同時(shí)可以加寬自動(dòng)模式下的音域,還可以實(shí)現(xiàn)對(duì)音長(zhǎng)的精準(zhǔn)控制,可謂是一舉多得。同時(shí)網(wǎng)上還有許多現(xiàn)成的midi文件,可以直接下載播放,不像我這個(gè)一樣,想演奏一首曲子時(shí)還要自己去網(wǎng)上找譜子,找到了還要自己一個(gè)個(gè)地手動(dòng)輸入到dat文件中,十分不便。
  2. 實(shí)現(xiàn)圖形界面。將手動(dòng)模式的界面變成動(dòng)態(tài)的,按下一個(gè)鍵時(shí)對(duì)應(yīng)的圖標(biāo)會(huì)有一定的動(dòng)畫(huà)效果,這樣的視覺(jué)反饋可以給用戶更好的使用體驗(yàn)。同時(shí)界面的跳轉(zhuǎn)、自動(dòng)模式下的暫停、播放、調(diào)整音量、快進(jìn)、后退等都可以做成按鈕,用鼠標(biāo)來(lái)操控,降低了用戶的操作難度。同時(shí)音符雨和滾動(dòng)字幕的實(shí)現(xiàn)也會(huì)簡(jiǎn)單一些。

9.源碼

/*
    開(kāi)始界面:手動(dòng)輸入or播放現(xiàn)有
    將簡(jiǎn)譜人肉轉(zhuǎn)碼成對(duì)應(yīng)鍵盤(pán)上的鍵的譜子,音符之間用空格隔開(kāi),使用fgetc讀取單個(gè)音符
    使用空格代表休止符
    每讀取一個(gè)音符,就新建一個(gè)線程并播放該音符.mp3,sleep一定的時(shí)間后--跟曲速有關(guān)--讀取下一個(gè)音符
    每個(gè)線程播放一定時(shí)間后自動(dòng)結(jié)束
*/
#include<thread>       
#include<iostream>
#include<string.h>
#include<windows.h>
#include<conio.h>
#include<mmsystem.h>
#include<stdlib.h>

using namespace std;

#pragma comment(lib,"winmm.lib") //[1]

#define MAXLEN 127
#define XSTART 16
#define YSTART 5
#define LENOFPAGE 146
#define DEPTHOFPAGE 20
short piano_type=1;    //2表示亮音鋼琴,1是原聲鋼琴

typedef union{
    int i;
    char c[10];
}CwithI;

typedef struct{
    char name[MAXLEN];
    CwithI qusu;
    char ver[20];
}HEAD;

void play_sound(char keyboard_key);//播放音頻
void play_song(FILE *puzi,short *ystart,HEAD mus_info,short *flag);//播放音樂(lè)
void decoding_func(char keyboard_key,char *sound_name,short piano_type);//解碼
void gotoxy(int x,int y);//移動(dòng)光標(biāo)至指定位置
void print_kuang();//打印框框
void print_pkeys();//打印手動(dòng)模式下的靜態(tài)鋼琴鍵盤(pán)
void cls_kuang(short,short);//清除框框內(nèi)指定行的信息
HEAD readhead(FILE *);//讀取譜子文件的信息頭
void HideCursor();//隱藏光標(biāo)
void ShowCursor();//顯示光標(biāo)
char begin_page();//初始界面
char exit_page();//退出界面
char manual_page();//手動(dòng)擋
char auto_page();//自動(dòng)擋

int main(){
    system("mode con cols=180 lines=38"); //[8]
    short mode=0;
    while(1){   //所有頁(yè)面的中轉(zhuǎn)站
        if(mode==0){
            mode = begin_page();
        }
        if(mode==1){
            mode = manual_page();
        }
        if(mode==2){
            mode = auto_page();
        }
        if(mode==3){
            exit_page();
            break;
        }
        fflush(stdin);
    }
    return 0;
}

char exit_page(){
    HideCursor();
    char sentences[][MAXLEN]={"PROGRAMME DESIGN",\
        "名字 from 院系 in 學(xué)校",\
        "MUSIC RESOURCE",\
        "autopiano.cn",\
        "SPECIAL THANKS TO",\
        "Professor 老師",\
        "T.A. 助教1",\
        "T.A. 助教2",\
        "室友",\
        "github.com/WarpPrism/AutoPiano",\
        "runoob.com/cprogramming/c-tutorial.html",\
        "",\/*最后一段字符串由于沒(méi)有下一段字符串來(lái)清掉它,會(huì)滯留在屏幕上,因此用空字符串來(lái)做最后一個(gè)*/
        "Thank You for Playing My Game!"\
    };
    print_kuang();
    short ystart=YSTART+DEPTHOFPAGE/2-11,line=0,sigofstr=0;
    int speed=500;
    for(line=YSTART+DEPTHOFPAGE-2;line+(sizeof(sentences)/MAXLEN-2)*2>YSTART;line--){
        for(sigofstr=0;sigofstr<sizeof(sentences)/MAXLEN-1;sigofstr++){
            if(kbhit()){
                speed = 0;   //快速跳過(guò)制作人員名單
            }
            if((line+sigofstr*2)<=YSTART+DEPTHOFPAGE-3&&(line+sigofstr*2)>=YSTART+2){
                cls_kuang(line+sigofstr*2,-1);
                cls_kuang(line+sigofstr*2,1);   //上下兩行都清除一下
                gotoxy(XSTART+LENOFPAGE/2-strlen(sentences[sigofstr])/2,line+sigofstr*2);
                cout << sentences[sigofstr];
            }
        }
        Sleep(speed);  //滾動(dòng)字幕的效果
    }
    for(line=YSTART+DEPTHOFPAGE-3;line>=YSTART+DEPTHOFPAGE/2-1;line--){
        cls_kuang(line,1);
        gotoxy(XSTART+LENOFPAGE/2-strlen(sentences[sizeof(sentences)/MAXLEN-1])/2,line);
        cout << sentences[sizeof(sentences)/MAXLEN-1];
        Sleep(speed);  //同上一條注釋
    }
    gotoxy(XSTART+LENOFPAGE/2-strlen("請(qǐng)按任意鍵繼續(xù). . .")/2,YSTART+DEPTHOFPAGE-2);
    system("PAUSE");
    return 0;
}

char begin_page(){
    char line_one[]="Welcome to elec-piano!";
    char line_two[]="choose your mode:1.manual   2.auto   3.exit";
    print_kuang();
    gotoxy(XSTART+LENOFPAGE/2-strlen(line_one)/2,YSTART+DEPTHOFPAGE/2-3);   //將光標(biāo)移到方框的正中央并打印文字
    cout << line_one;
    gotoxy(XSTART+LENOFPAGE/2-strlen(line_two)/2,YSTART+DEPTHOFPAGE/2-1);
    cout << line_two;
    gotoxy(XSTART+LENOFPAGE/2,YSTART+DEPTHOFPAGE/2+1);   //將光標(biāo)移至中央
    char mode[127]={0};
    while(1){
        cin >> mode;     //輸入檢測(cè)
        while(getchar()!='\n'){
            continue;
        }
        if(mode[0]>='1'&&mode[0]<='3'&&strlen(mode)==1){
            break;
        }
        else{
            cls_kuang(YSTART+DEPTHOFPAGE/2+3,0);
            gotoxy(XSTART+LENOFPAGE/2-strlen("Input error!please try again:")/2,YSTART+DEPTHOFPAGE/2+1);
            cout << "Input error!please try again:" << endl;
            gotoxy(XSTART+LENOFPAGE/2,YSTART+DEPTHOFPAGE/2+3);
        }
    }
    system("CLS");
    return (mode[0]-'0');
}

char auto_page(){
    FILE *dir,*puzi,*puzi_hx;
    char sys_dir_msg[MAXLEN];
    char mus_namelist[50][MAXLEN]={0};
    short numofsong=0,ystart=YSTART+1,xstart=XSTART+1,lenofsongname=0;
    HEAD mus_info,hx_info;
    print_kuang();
    gotoxy(XSTART+1,ystart);
    cout << "Choose your music:";
    gotoxy(XSTART+1,++ystart);
    dir = _popen("dir .\\songs","r");
    while(!feof(dir)){
        fscanf(dir,"%s",sys_dir_msg);
        if(strstr(sys_dir_msg,".dat")!=NULL){//列出songs文件夾中的dat文件
            strcpy(mus_namelist[numofsong],sys_dir_msg);
            cout << ++numofsong << "." << sys_dir_msg;
            if(strlen(sys_dir_msg)>lenofsongname){
                lenofsongname = strlen(sys_dir_msg);
            }
            gotoxy(xstart,++ystart);
            if(ystart==DEPTHOFPAGE+YSTART-1){ //文件過(guò)多時(shí)換個(gè)行繼續(xù)
                xstart += lenofsongname+5;
                ystart = YSTART+2;
                lenofsongname = 0;
                gotoxy(xstart,ystart);
            }
        }
    }
    fclose(dir);
    CwithI tempci;
    while(1){
        scanf("%9s",tempci.c);//輸入檢測(cè)
        while(getchar()!='\n'){
            continue;
        }
        tempci.c[9] = '\0';
        tempci.i = atoi(tempci.c);
        if(tempci.i==0||tempci.i>50||tempci.i<0){
            cls_kuang(ystart,1);
            cout << "Input Error!please try again:";
        }
        else{
            break;
        }
    }
    thread t(play_sound,'+');   //播放一段無(wú)聲的MP3以加載播放器相關(guān)dll
    t.detach();
    gotoxy(XSTART+1,++ystart);
    char path_mode[50]="songs\\%s";
    char path[50]={0};
    char path_hx[50]={0};
    sprintf(path,path_mode,mus_namelist[tempci.i-1]);
    strcpy(path_hx,path);
    path_hx[strlen(path)-1] = 'x';
    path_hx[strlen(path)-2] = 'h';
    puzi = fopen(path,"r");
    
    if(puzi==NULL){
        cout << "File does not exist! Check your input";
        Sleep(2500);
        system("cls");
        return 0;
    }
    short hx_flag=1;
    puzi_hx = fopen(path_hx,"r");
    if(puzi_hx==NULL){
        hx_flag = 0;
    }
    else{
        hx_info = readhead(puzi_hx);
    }
    mus_info = readhead(puzi);
    
    system("cls");
    print_kuang();
    ystart=YSTART+1;
    gotoxy(XSTART+1,ystart);
    cout << "now play: " << mus_info.name;
    gotoxy(XSTART+1,++ystart);
    short flag=0;
    if(hx_flag==1){//有和弦時(shí)進(jìn)入此分支
        while(1){
            thread t1(play_song,puzi,&ystart,mus_info,&flag);
            thread t2(play_song,puzi_hx,&ystart,hx_info,&hx_flag);
            t2.detach();
            t1.join();
            if(feof(puzi)||feof(puzi_hx)||flag==2||hx_flag==2){//其中一個(gè)譜子放完了自動(dòng)跳出整個(gè)循環(huán)
                break;
            }
        }
    }
    else{
        while(1){
            thread t1(play_song,puzi,&ystart,mus_info,&flag);
            t1.join();
            if(feof(puzi)||flag==2){
                break;
            }
        }
    }
    fclose(puzi);
    if(hx_flag!=0){
        fclose(puzi_hx);
    }
    
    gotoxy(XSTART+1,++ystart);
    cls_kuang(ystart,1);
    cout << "end" << endl;
    cls_kuang(++ystart,1);
    
    system("PAUSE");
    system("CLS");
    return 0;
}

char manual_page(){
    HideCursor();
    print_kuang();
    print_pkeys();
    gotoxy(XSTART+1,YSTART+DEPTHOFPAGE-2);
    cout << "press Enter to quit the manual mode!";
    thread t(play_sound,'+');   //播放一段無(wú)聲的MP3以加載播放器相關(guān)dll
    t.detach();
    int i=0;
    while(1){
        char keyboard_key;
        keyboard_key = getch(); //[2]
        if(keyboard_key=='\r'){
            break;     //按回車退出
        }
        thread t(play_sound,keyboard_key); //[3]
        t.detach();
        FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE)); //[4]
    }
    gotoxy(XSTART+LENOFPAGE/2-strlen("exit")/2,YSTART+DEPTHOFPAGE/2);
    cout << "exit!";
    ShowCursor();
    system("CLS");
    return 0;
}

void decoding_func(char keyboard_key,char *sound_name,short piano_type){ //在plays函數(shù)中被引用,可有效降低創(chuàng)建線程時(shí)傳參的復(fù)雜程度
    char jian[]={'1','!','2','@','3','4','$','5','%','6','^','7','8','*','9','(','0','q','Q',\
        'w','W','e','E','r','t','T','y','Y','u','i','I','o','O','p','P','a','s','S','d','D',\
        'f','g','G','h','H','j','J','k','l','L','z','Z','x','c','C','v','V','b','B','n','m'}; //所有音名對(duì)應(yīng)的按鍵
    char zhi_zimu[][3]={"C","Cs","D","Ds","E","F","Fs","G","Gs","A","As","B"}; //音名字母部分
    char zhi_shuzi[][2]={"0","1","2","3","4","5","6","7"};  //音名數(shù)字部分
    
    for(short i=0;i<sizeof(jian);i++){
        if(keyboard_key==jian[i]){
            short zimu_bianhao=i%12;   //字母部分12個(gè)一輪回
            short shuzi_bianhao=i/12;    //數(shù)字部分每十二個(gè)音+1
            char ss_zimu[5],ss_shuzi[2];
            strcpy(ss_zimu,zhi_zimu[zimu_bianhao]);
            strcpy(ss_shuzi,zhi_shuzi[shuzi_bianhao+piano_type]);  //帶一個(gè)piano_type補(bǔ)正可以拓展音域
            strcat(ss_zimu,ss_shuzi);
            strcpy(sound_name,ss_zimu);
        }
        else{
            continue;
        }
    }
}

void play_sound(char keyboard_key){  
    char sound_name[5]={0};    //音名
    char temp_command[MAXLEN]={0};     //mciSendString的命令
    if(keyboard_key=='+'){
        strcpy(sound_name,"No_sound");    //播放一段無(wú)聲的聲音,用于加載與播放器有關(guān)的DLL文件
    }
    else{
        decoding_func(keyboard_key,sound_name,piano_type);   //將鍵盤(pán)上的鍵對(duì)應(yīng)地解碼成音名
    }
    sprintf(temp_command,"open piano\\%s.mp3 alias %s",sound_name,sound_name);
    mciSendStringA(temp_command,0,0,0); //[5] //打開(kāi)音名.mp3
    sprintf(temp_command,"play %s",sound_name);
    mciSendStringA(temp_command,0,0,0);    //播放
    Sleep(10000);
    sprintf(temp_command,"close %s",sound_name);   //關(guān)閉
    mciSendStringA(temp_command,0,0,0);
    return;
}

void play_song(FILE *puzi,short *ystart,HEAD mus_info,short *flag){
    char keyboard_key;
    keyboard_key = fgetc(puzi);
    if(*flag==0){
        cout << keyboard_key;
    }
    if(keyboard_key=='|'||keyboard_key=='\n'){//遇到|和換行符時(shí),由于不對(duì)樂(lè)曲本身發(fā)揮任何作用,因此需要特殊處理
        if(keyboard_key=='\n'&&(*flag)==0){
            if((*ystart)+1==DEPTHOFPAGE+YSTART-1){
                *ystart=YSTART+2;
                cls_kuang(*ystart,1);
            }
            else{
                if((*ystart)+2==DEPTHOFPAGE+YSTART-1){
                    (*ystart)++;
                    gotoxy(XSTART+1,*ystart);
                }
                else{
                    (*ystart)++;
                    cls_kuang(*ystart,1);
                }
            }
        }
        return;   
    }
    else{
        Sleep(mus_info.qusu.i);//此處控制曲速,注意:與一般音樂(lè)軟件使用的曲速單位bpm不同,這里只是單純的停頓一定毫秒
    }
    if(kbhit()){
        char kbout='\0';
        kbout = _getch();
        if(kbout=='\r'){ //播放中途按回車鍵退出
            *flag = 2;
            return;
        }
    }
    thread t(play_sound,keyboard_key);  //單獨(dú)分出線程來(lái)播放聲音
    t.detach();    
    return;
}

void gotoxy(int x,int y){    //[6]
    _COORD pos;
    pos.X=x;
    pos.Y=y;
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),pos);  //設(shè)置鼠標(biāo)位置
}

void print_kuang(){
    short start_x=XSTART,start_y=YSTART;
    short page_x=LENOFPAGE,page_y=DEPTHOFPAGE;
    gotoxy(start_x,start_y);      //將鼠標(biāo)移動(dòng)到起始點(diǎn)
    for(short y=0;y<page_y;y++){
        if(y==0||y==page_y-1){
            gotoxy(start_x,start_y+y);
            for(short x=0;x<=page_x;x++){
                cout << "=";     //若為第一行或最后一行,則打印page_x個(gè)=
            }
        }
        else{
            gotoxy(start_x,start_y+y);
            cout << '|';
            gotoxy(start_x+page_x,start_y+y);
            cout << '|';      //每行開(kāi)頭和末尾打印|
        }
    }
}

void print_pkeys(){
    short ystart=YSTART+1;
    gotoxy(XSTART+1,ystart);//普通的打?。ê艽溃?    cout << "_________________________________________________________________________________________________________________________________________________";
    for(short i=0;i<6;i++){
        gotoxy(XSTART+1,++ystart);
        cout << "|  | | | |  |  | | | | | |  |  | | | |  |  | | | | | |  |  | | | |  |  | | | | | |  |  | | | |  |  | | | | | |  |  | | | |  |  | | | | | |  |   |";
    }
    gotoxy(XSTART+1,++ystart);
    cout << "|  |!| |@|  |  |$| |%| |^|  |  |*| |(|  |  |Q| |W| |E|  |  |T| |Y|  |  |I| |O| |P|  |  |S| |D|  |  |G| |H| |J|  |  |L| |Z|  |  |C| |V| |B|  |   |";
    for(short i=0;i<2;i++){
        gotoxy(XSTART+1,++ystart);
        cout << "|  | | | |  |  | | | | | |  |  | | | |  |  | | | | | |  |  | | | |  |  | | | | | |  |  | | | |  |  | | | | | |  |  | | | |  |  | | | | | |  |   |";
    }
    gotoxy(XSTART+1,++ystart);
    cout << "|  |_| |_|  |  |_| |_| |_|  |  |_| |_|  |  |_| |_| |_|  |  |_| |_|  |  |_| |_| |_|  |  |_| |_|  |  |_| |_| |_|  |  |_| |_|  |  |_| |_| |_|  |   |";
    for(short i=0;i<2;i++){
        gotoxy(XSTART+1,++ystart);
        cout << "|   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |";
    }
    gotoxy(XSTART+1,++ystart);
    cout << "| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | q | w | e | r | t | y | u | i | o | p | a | s | d | f | g | h | j | k | l | z | x | c | v | b | n | m |";
    gotoxy(XSTART+1,++ystart);
    cout << "| C2| D2| E2| F2| G2| A2| B2| C3| D3| E3| F3| G3| A3| B3| C4| D4| E4| F4| G4| A4| B4| C5| D5| E5| F5| G5| A5| B5| C6| D6| E6| F6| G6| A6| B6| C7|";
    gotoxy(XSTART+1,++ystart);
    cout << "|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|";
}

void cls_kuang(short line,short mode){    
    gotoxy(XSTART+1,line);//清除框框內(nèi)的文字,比system(“cls”)更快
    for(short c=0;c<LENOFPAGE-1;c++){
        printf(" ");
    }
    gotoxy(XSTART+1,line+mode);//清除當(dāng)前行數(shù)之外的另一行
    for(short c=0;c<LENOFPAGE-1;c++){
        printf(" ");
    }
    gotoxy(XSTART+1,line);//光標(biāo)回到初始位置
}

void HideCursor(){  //[7]
    CONSOLE_CURSOR_INFO cursor;    //隱藏光標(biāo)
    cursor.bVisible = FALSE;    
    cursor.dwSize = sizeof(cursor);    
    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);    
    SetConsoleCursorInfo(handle, &cursor);
}

void ShowCursor(){
    CONSOLE_CURSOR_INFO cursor;    //顯示光標(biāo)
    cursor.bVisible = TRUE;    
    cursor.dwSize = sizeof(cursor);    
    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);    
    SetConsoleCursorInfo(handle, &cursor);
}

HEAD readhead(FILE *puzi){  //讀取曲譜文件的標(biāo)頭,獲取曲速,曲名等信息
    char flag=0,keyboard_key,count=0; 
    char tempstr[MAXLEN]={0};
    HEAD temphead;
    while(!feof(puzi)){
        keyboard_key = fgetc(puzi);
        if(keyboard_key=='<'){
            flag++;      
            continue;
        }
        if(keyboard_key=='>'){
            count = 0;
            flag++;        //使得變量flag兼具計(jì)數(shù)和條件判斷的功能
            if(flag==2){
                strcpy(temphead.name,tempstr);
            }
            if(flag==4){
                strcpy(temphead.qusu.c,tempstr);
                temphead.qusu.i = atoi(temphead.qusu.c);
            }
            if(flag==6){
                strcpy(temphead.ver,tempstr);
                keyboard_key = fgetc(puzi);   //將信息頭最后一個(gè)換行符給吃掉
                break;
            }
            memset(tempstr,'\0',sizeof(tempstr));
        }
        if(flag%2==1){
            tempstr[count++] = keyboard_key;
        }
    }
    return temphead;
}


/*
參考文獻(xiàn):
[1]在C語(yǔ)言控制臺(tái)程序中播放MP3音樂(lè),https://www.cnblogs.com/honkly/p/3738022.html,2019-12-01.
[2]C++之 _getch()和getchar()的區(qū)別,https://blog.csdn.net/MrHHHHHH/article/details/89329984,2019-12-02.
[3]C++使用thread類多線程編程,https://www.cnblogs.com/qinwanlin/p/thread.html,2019-12-01.
[4]怎樣清空鍵盤(pán)緩沖區(qū)?,https://qa.codeabc.cn/questions/detail/107,2019-12-02.
[5]mciSendString()用法,https://blog.csdn.net/xionglifei2014/article/details/80222078,2019-12-03.
[6]C++得到光標(biāo)坐標(biāo)和移動(dòng)光標(biāo),https://www.cnblogs.com/noevil/archive/2010/10/12/1849092.html,2019-12-06.
[7]C++隱藏光標(biāo),https://blog.csdn.net/qq_41222732/article/details/97145818,2019-12-12.
[8]C語(yǔ)言如何控制控制臺(tái)窗口大小,https://blog.csdn.net/ZouHuiDong/article/details/89812472,2019-12-19.
*/

命名和注釋之類的不是很規(guī)范,一些循環(huán)也使用了比較笨的處理方法,獻(xiàn)丑了
附上平凡之路的譜子:(保存為ordinary_road.dat)

<ordinary road><170><Awhx>
.   .   |
.   u t t   i t |t   u t r t y w |.   u t t   i t |
t   u t r t y w |. u u pp. t y uu|.   .   -   -   |
. u u pp. oo. iu|.   .   -   -   |. u u p . t y u |
.   .   -   -   |. u u t u ii.u  |t   .   -   -   |
. u u pp. t y uu|.   .   -   -   |. u u pp. oo. iu|
.   .   -   -   |. u u p . t y u |.   .   -   -   |
. u u t i ii.u  |t   .   . o p a |s as.op p   o iu|
. u u uy. o p a |s as.op p   p ss|. s s sd. .op a |
s as.f  j  h. g |f   f d .   p a |s   s as.  sas d|
.   s as.   .   |.   u t .   i t |.   u t r t y w |
.   u t .   i t |.   u t r t y w |. f f jj. s d ff|
.   .   -   -   |. f f jj. hh. gf|.   .   -   -   |
. fff j . ssd f |.   .   -   -   |. f f s g gg. f |
s   .   . h j k |l kl.hj j   h gf|. f f fd. h j k |
l kl.hj j   j kl|. l l lz. .hj k |l kl.x  b  v. c |
x   x z .   j k |l  ll zl.  lkl z|.   l kl   .   .|
h as. has asa o |h as. d a opa o |h as. oas asd h |
h as. ha.sd .   |. as. sas asa o |. os. osa opa o |
h as. has dfg f |s osd o dfg f   |
asaso s s asa o |s aso s s apa o |
. ass oas asd   |. ass oaasdgd   |
. as. oas asa o |h ghf d g adf s |
asas. oas dfg f |g fdd hddfg f   |. s s s . s s s |
. s s s . a a s |. s s s . s s s |. s s s . a a s |
. s s s . s s s |. s s s . a a s |. s s s . s s s |
. s s s . h j k |l kl.hj j   h gf|. f f fd. h j k |
l kl.hj j   j kl|. l l lz. h j k |l kl.x  b  v. c |
x   x z .   j k |l   l zl.  lkl z|.   l kl. h j k |
l kl.hj j   h gf|. f f fd. h j k |
l kl.hj j   j kl|. l l lz. h j k |
l kl.x  b  v. c |x   x z .   j k |
l   l zl.  lkl z|.   l kl. h j k |
l kl.hj j   h gf|. f f fd. h j k |
l kl.hj j   j kl|. l l lz. h j k |
l kl.x  b  v. c |x   x z .   j k |
l   l zl.  lkl z|.   l kl.   .   |
. u u pp. t y uu|.   .   -   -   |. u u pp. oo. iu|
.   .   -   -   |. uuu p . tty u |.   .   -   -   |
. u u t i ii.u  |. t     -   -   |-   -   |

及其和弦:(保存為ordinary_road.dhx)

<ordinary road><170><hx>
.   .   |
6 e     4 q     |8 w     5   .   |6 e     4 q     |
8 w     5   .   |6 e   t 4 q   t |8 w u t r t y w |
6 e   t 4 q t q |8 w u t r t y w |6 e   u 4 q   t |
8 w u t r t y w |6 e   0 4 8 q 8 |8 w u t r t y   |
6 e   t 4 q t q |8 w u t r t y w |6 e   u 4 q t q |
8 w u t r t y w |6 e   u 4 q   t |8 w u t r t y w |
6 e   0 4 8 q 8 |8 w u t w t y   |6 e u t 4 q t q |
8 w t w 5 w y w |6 e u t 4 q t t |8 w u w 5 w y w |
6 e u t 4 q t q |8 w u w 5 w y w |6 e u t 4 q t q |
5 w y w 8 w t   |6 e     4 q     |8 w     5   .   |
6 e     4 q     |8 w     5   .   |6 e u t 4 q t q |
8 w u t r t y w |6 e u t 4 q t q |8 w u t r t y w |
6 e u t 4 q t q |8 w u t r t y w |6 e u 0 4 8 q 8 |
8 w u t w 8 9 5 |6 e u t 4 q t q |8 w t w 5 w y w |
6 e u t 4 q t t |8 w t w 5 w y w |6 e u t 4 q t q |
8 w t w 5 w y w |6 e u t 4 q t q |5 w y w 8 w t t |
6 e u t 4 q t q |8 w u t 5 w y w |6 e u t 4 q t q |
8 w u t 5 w t w |6 e u t 4 q t q |8 w u t 5 w y w |
6 e u t 4 q t q |8 w u t 5 w y w |
6 e u t 4 q t q |8 w u t 5 w y w |
6 e u t 4 q t q |8 w u t 5 w y w |
6 e u t 4 q t q |8 w u t 5 w y w |
6 e u t 4 q t q |8 w u t 5 w y w |6 e   t 4 q   t |
1 w t w 5 w   5 |6 e   t 4 q   t |1 w t w 5 w   5 |
6 6   6 4 4   4 |1 w t w 5 5   5 |6 6   6 4 4   4 |
1 w t w 5 5 q w |6 e u t 4 q t q |8 w t w 5 w y w |
6 e u t 4 q t q |8 w t w 5 w y w |6 e u y 4 q t q |
8 w t w 5 w y w |6 e u t 4 q t q |5 w y w 8 w tw84|
60etu0t048qetqeq|8wtyuwtw59wryrw0|
60etu0t048qetqeq|8wtyuwtw59wryrw0|
60etu0t048qetqeq|8wtyuwtw59wryrw0|
60etu0t048qetqeq|59wrtyoy1 5 8 5 |
60etu0t048qetqeq|8wtyuwtw59wryrw0|
60etu0t048qetqeq|8wtyuwtw59wryrw0|
60etu0t048qetqeq|8wtyuwtw59wryrw0|
60etu0t048qetqeq|59wrtyoy8wtyo t |
6 e   t 4 q   t |8 w u t r t y w |6 e   t 4 q t q |
8 w u t r t y w |6 e   u 4 q   t |8 w u t r t y w |
6 e   0 4 8 q 8 |1 5 9 8 r t y w |.   .   |

10.結(jié)語(yǔ)

寫(xiě)到這里才發(fā)現(xiàn)寫(xiě)了好多,真的會(huì)有人完整地看完嗎。。。。。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容