前言
應朋友之邀,寫一篇分析原神風物之詩自動彈奏原理的博客,並附帶一個樣例程序。
朋友的要求是:只用大一的C語言知識,寫一個類似C語言大項目的程序,來實現原神風物之詩自動彈奏的功能。
注1:閱讀本文需具有前置知識:C語言數組、文件、指針的知識。
注2:樣例程序對編譯器無限制,雖然dev-c++也能編譯,但建議使用Visual Studio。
注3:本類程序使用C並不是最優解,相比來說,java更適合本類程序。
注4:文中的分析與注釋基於初學者的C語言水平,實現簡單,用語白話,程序結構沒有進行精簡。
正文
先來分析一下常見的風物之詩自動彈奏程序(pc端)。
(圖源網絡,侵權必刪)
不難看出,這些程序本質上都由配置、選擇樂譜、進行播放這幾部分組成。再拆分一下進行模塊化,我們將程序大體分為以下幾個模塊:主菜單模塊menu(),調節曲速模塊adjust(),集成文件選擇與樂譜播放的讀譜演奏模塊play()。
第一部分是menu模塊,功能是列出程序功能並提供相應跳轉。本部分涉及對輸入的處理操作。menu模塊演示如下:
第二部分是adjust模塊,模塊功能是修改程序中的一個變量(音間間隔變量cd)。本部分涉及指針操作。adjust模塊演示如下:
第三部分是play模塊,模塊主要提供兩個功能。第一個功能是遍歷特定文件夾里的特定樂譜文件,列出樂譜文件供用戶選擇。本部分涉及一維、二維數組操作和文件操作。
第二個功能是根據用戶選擇的樂譜進行讀譜彈奏。本部分涉及數組、文件以及模擬按鍵操作。
代碼實現
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <windows.h> 4 5 int cd=300; //音間間隔 6 char temp; //1號臨時變量 7 int temp2; //2號臨時變量 8 int *p= &cd;//指向音間間隔的指針,需要對大一朋友強調的是,一定要有初始化指針的習慣,以免出現野指針 9 10 //play函數內的變量 11 char c;//臨時變量,儲存模擬按下的鍵的鍵名 12 int tempcd;// tempcd為臨時的音間間隔,方便調整。 13 char musicPath[20]="樂譜\\*.txt"; //樂譜文件的地址的通用格式 14 WIN32_FIND_DATA findFileData;//文件操作常用的一種數據結構 15 HANDLE hFind;//句柄 16 char str[50];//記錄樂譜名字的字符串, 歌名最長50字符,漢字占兩個 17 char path[55][50]; //記錄樂譜名字的字符串數組,用二維數組實現,最多55首歌 18 19 void adjust(int *p);//調節曲速(音間間隔)功能 20 void menu();//主菜單 21 void play();//彈奏樂譜功能 22 void me();//項目背景 23 void bye();//退出菜單 24 25 void menu()//主菜單 26 { 27 printf("\n --------------功能菜單--------------\n"); 28 printf(" | |\n"); 29 printf(" | 1、調節曲速 |\n"); 30 printf(" | 2、讀譜演奏 |\n"); 31 printf(" | 3、項目相關 |\n"); 32 printf(" | 4、退出程序 |\n"); 33 printf(" | |\n"); 34 printf(" ------------------------------------\n"); 35 temp = getch();//getch會讀取下一個鍵入的字符,無需回車 36 if(temp<'1' || temp>'4')//規范輸入 37 { 38 printf(" 請輸入1-4之間的整數 \n\n"); 39 menu(); 40 } 41 if(temp == '1')//頁面跳轉 42 { 43 adjust(&cd); 44 } 45 if(temp == '2') 46 { 47 play(); 48 } 49 if(temp == '3') 50 { 51 me(); 52 } 53 if(temp == '4') 54 { 55 bye(); 56 } 57 } 58 59 void adjust(int *p)//使用指針可以將在函數內部改變的音間間隔傳遞出函數,當然,也可以用返回值解決這個問題 60 { 61 printf("\n--------------調節曲速--------------\n"); 62 printf("| |\n"); 63 printf("| 這里的曲速指兩個音之間的時間間隔 |\n"); 64 printf("| 當前值%3ld,單位ms,值越小演奏越快 |\n",cd); 65 printf("| 將新曲速調節為: __________ ms |\n"); 66 printf("| |\n"); 67 printf("------------------------------------\n "); 68 scanf("%d",&temp2);//將新曲速賦給臨時變量temp2 69 while('\n'!=getchar()); //對可能殘留在輸入流中的數據進行清理。 70 if(temp2>0 && temp2<5000)//將新曲速限制在0~5s 71 { 72 *p = temp2;//符合要求則將新曲速賦給音間間隔cd 73 printf(" 新曲速( %d ms)設置成功 \n\n",cd); 74 menu();//跳轉 75 }else 76 { 77 printf(" 請輸入1-5000之間的整數 \n",cd); 78 printf(" 單位 ms ,1000 ms = 1 s \n\n",cd); 79 adjust(&cd); //規范輸入 80 } 81 } 82 83 void play()//遍歷樂譜文件夾,列出樂譜,使用者選擇后,讀相應樂譜,然后模擬按鍵彈奏。此部分可進一步模塊化 84 { 85 int i=0;//記錄樂譜序號 86 int j=0;//記錄選擇的樂譜的序號的臨時變量 87 char str[50] = "樂譜/";//初始化樂譜地址字符串,初始化為樂譜地址的目錄 88 char path[55][50]={};//初始化樂譜名字符串數組,初始化為空 89 hFind=FindFirstFile(musicPath,&findFileData);//找到目錄下第一個格式為 musicPath(即樂譜\\*.txt)的文件 90 if(hFind==INVALID_HANDLE_VALUE) //如果目錄下沒有(第一個)文件 91 { 92 printf("樂譜文件夾中沒有文件,請將txt格式的樂譜放入樂譜文件夾中!\n"); 93 menu(); 94 } 95 else //如果目錄下存在(第一個)文件 96 { 97 printf("當前可演奏的樂譜有:\n"); 98 while(1)//開始遍歷樂譜,並將每個樂譜的地址放進path數組中。 99 { 100 if(findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) 101 {//優化搜索結果,可以注釋掉這個ifelse看看 102 } 103 else //輸出符合結果文件名 104 { 105 char str[50] = "樂譜/";//新一輪循壞開始的時候重置str 106 memcpy(str+5, findFileData.cFileName, strlen(findFileData.cFileName));//補全樂譜名字符串str,添加目錄后的樂譜名,memcpy會據參數進行字符串覆蓋,詳情可 107 //百度,第一個參數+5值覆蓋的內容從str的第六個字符開始,目的是保留"樂譜/" 108 sprintf(path[i],str);//將得到的樂譜名str放入樂譜名數組path,也可以用memcpy(path[i], str, strlen(str))。path[i]可以看作path[i][]的數組名 109 i++;//數組下標從0開始,供用戶選擇的樂譜序號從1開始,所以i++放到這個位置 110 printf("%3d : %s\n",i,findFileData.cFileName); //輸出序號和與其對應的樂譜名 111 } 112 if(!FindNextFile(hFind,&findFileData)) //沒有下一個文件了就退出遍歷 113 { 114 break; 115 } 116 }FindClose(hFind); //關掉句柄 117 printf(" 輸入樂譜前序號開始演奏:\n"); 118 scanf(" %d",&j);//讀者可以自行對比這里的讀取輸入與主菜單的讀取輸入的區別 119 while('\n'!=getchar()); //對可能殘留在輸入流中的數據進行清理。 120 if(j<1 || j>i) 121 { 122 printf(" 請輸入1-4之間的整數 \n\n"); 123 play(); 124 }else 125 { 126 FILE *p = fopen(path[j-1], "r");//打開文件流,因為給用戶看的序號比數組的下標大1,所以這里j-1 127 if(p == NULL) 128 { 129 printf(" 樂譜 %s 打開失敗!\n",path[j-1]); 130 play(); 131 } 132 printf(" 5秒后開始演奏 %s ,請切到原神風物之詩界面...\n\n",path[j-1]); 133 Sleep(5000);//暫停5秒 134 tempcd=cd;//tempcd為臨時的音間間隔。 135 printf(" 演奏開始...\n"); 136 printf(" ...\n"); 137 //while((c = fgetc(p)) != EOF) 138 // while(fscanf(p, "%c,%d", &c, &a) != EOF)//可以對比一下不同循環的區別 139 for(;fscanf(p, "%c%d", &c, &tempcd) != EOF;tempcd=cd)//第二個參數:格式化讀取到文件結束;第三個參數:沒有指定音間間隔的使用默認音間間隔tempcd 140 { 141 if(c>='a'&&c<='z') c-=32;//小寫轉大寫 142 if(c>='A'&&c<='Z') 143 { 144 keybd_event(c, 0, 0, 0); //模擬按下鍵盤 145 keybd_event(c, 0, 2, 0); //模擬松開鍵盤 146 } 147 Sleep(tempcd);//等待指定時間:音間間隔tempcd毫秒。放到if外面,這樣文檔中打','來分隔的話也會延時默認音間間隔 148 //printf(" %c\n %d\n",c,tempcd);//可以解除注釋觀察讀文件過程 149 } 150 fclose(p);//關閉文件流 151 printf("\n %s 演奏完畢!\n\n",path[j-1]); 152 } 153 } 154 printf(" -----------------------\n"); 155 printf(" | |\n"); 156 printf(" | 現在你可以: |\n"); 157 printf(" | 1、繼續演奏 |\n"); 158 printf(" | 2、返回首頁 |\n"); 159 printf(" | 3、退出程序 |\n"); 160 printf(" | |\n"); 161 printf(" -----------------------\n"); 162 j = 0;//重置臨時變量 163 scanf(" %d",&j); 164 while('\n'!=getchar()); //對可能殘留在輸入流中的數據進行清理。 165 if(j<1 || j>3) 166 { 167 printf(" 請輸入1-3之間的整數 \n\n"); 168 play(); 169 } 170 if(j == 1)//跳轉 171 play(); 172 if(j == 2) 173 menu(); 174 if(j == 3) 175 bye(); 176 } 177 178 void me() 179 { 180 printf("\n"); 181 FILE *file = fopen("項目相關.txt", "r"); 182 if(file == NULL) 183 printf("文件缺失!\n"); 184 char b; 185 while((b = fgetc(file)) != EOF) 186 printf("%c", b); 187 fclose(file); 188 printf("\n"); 189 menu(); 190 } 191 192 void bye() 193 { 194 printf("\n \n"); 195 printf(" ---------------------- \n"); 196 printf(" | 我們都有光明的未來 | \n"); 197 printf(" ---------------------- \n"); 198 printf(" \n"); 199 printf(" ------ ------ \n"); 200 printf(" | 我 | /\\ | 你 |\n"); 201 printf(" | 單 | /福\\ | 十 |\n"); 202 printf(" | 抽 | /福福福\\ | 連 |\n"); 203 printf(" | 奇 | <福福福福福> | 七 | \n"); 204 printf(" | 跡 | \\福福福/ | 金 |\n"); 205 printf(" | 一 | \\福/ | 從 |\n"); 206 printf(" | 身 | \\/ | 未 |\n"); 207 printf(" | 歐 | | 保 | \n"); 208 printf(" | 氣 | | 底 | \n"); 209 printf(" ------ ------ \n"); 210 printf(" \n"); 211 printf(" \n"); 212 printf(" 希望你喜歡這個不完善的小程序 \n"); 213 printf(" 再見啦 /派蒙揮手.gif \n\n"); 214 system("pause"); 215 } 216 217 void main() 218 { 219 menu(); 220 }
原理講解到此就差不多了,如果有沒講清楚的地方歡迎提出。另外,程序的具體的使用方法我會和源碼、樂譜庫一起放到百度雲中,有需要的朋友自取。
補充
如果你覺得程序太簡陋(確實太簡陋了),那么可改進的地方還有很多,比如使用變長數組,再增加暫停、錄譜、和音等功能;或者改進程序使之不僅能讀鍵盤譜,還能讀數字譜,並增加數據庫操作來處理兩種譜;也可以使用圖形化api來寫個窗口程序,同時讓程序自動獲得管理員權限等。難度逐漸上升,但都是大一經過不復雜的學習或百度就能實現的功能,但是像分析音頻文件自動生成樂譜文件等功能這里就不推薦了,因為這種功能往往涉及方向特化的知識,有興趣的朋友可以自行實現。
另外,我在米游社發布了一篇匯總對比風物之詩自動彈奏方法的帖子,有興趣的朋友可以去看一下。鏈接:https://bbs.mihoyo.com/ys/article/5395350?create=1。
最后,我的UID是116752259,我的世界還蠻大的,資源都可以隨便拿,歡迎你來我家玩。
百度網盤鏈接:https://pan.baidu.com/s/1Jl6F3sHnRXAkc00YXMtbkg
提取碼:1234