前言
应朋友之邀,写一篇分析原神风物之诗自动弹奏原理的博客,并附带一个样例程序。
朋友的要求是:只用大一的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