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ā)者工具包

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


引用頭文件或類:iostream, thread, string.h, windows.h, conio.h, mmsystem.h, stdlib.h
鏈接的庫(kù):winmm.lib
計(jì)算機(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)上一些在線鋼琴:

發(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ǔ)人肉解碼好的譜子?
如圖:

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

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è)初始界面:

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

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

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

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

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

8.反思
雖然程序是寫(xiě)了出來(lái),而且能夠良好運(yùn)行,但與預(yù)期還是有較大差別:
- 未能實(shí)現(xiàn)像網(wǎng)頁(yè)上的那些鋼琴一樣,手動(dòng)模式下按下一個(gè)鍵時(shí)對(duì)應(yīng)圖形會(huì)發(fā)生變化,以表示按下。
- 未能實(shí)現(xiàn)像網(wǎng)頁(yè)上的一樣,自動(dòng)播放時(shí)有一個(gè)音符雨的效果。且自動(dòng)播放時(shí)只能在屏幕上顯示主音軌的對(duì)應(yīng)按鍵,無(wú)法顯示另一個(gè)音軌的按鍵,不利于用戶學(xué)習(xí)。
- 對(duì)于音軌的可拓展性較弱。雙音軌對(duì)于鋼琴來(lái)說(shuō)是遠(yuǎn)遠(yuǎn)不夠的,現(xiàn)實(shí)中的鋼琴有的時(shí)候甚至?xí)瑫r(shí)產(chǎn)生五六個(gè)音。
- 播放音頻較為密集的時(shí)候會(huì)出現(xiàn)卡頓的情況,但此時(shí)CPU還是非常的空閑,可能是出現(xiàn)了所謂的“一核有難,七核圍觀”現(xiàn)象。如何充分的調(diào)動(dòng)CPU分擔(dān)計(jì)算任務(wù),是一個(gè)非常值得思考的問(wèn)題。
- 受電腦鍵盤(pán)所限,音域較為狹窄,也讓熟悉鋼琴的人彈起來(lái)不怎么習(xí)慣。
- 未能實(shí)現(xiàn)圖形化界面,并不能稱得上是一款真正的游戲。
因此,如果要改進(jìn)的話,我認(rèn)為可以從以下幾個(gè)方面入手:
- 增加對(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文件中,十分不便。
- 實(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ì)有人完整地看完嗎。。。。。。