用C/C++写一个简易的钢琴小程序

0.缘由

1.环境

Win10系统

Micosoft cl编译器,msvc开发者工具包

2.思路

思考编写过程中可能会出现的问题和需要特别关照的点:

2.1如何在程序内播放声音/h3>

上 查了一下,似乎是在mmsystem.h头文件中,提供了一个windows本身的api函数mciSendString,可以播放媒体文件。但不幸的是,使用这个函数需要链接一个动态库,而我一直以来使用的g++编译器不仅链接起来十分麻烦,g++自带的那些库中还找不到这个库。many shoes了一下,大概是说gcc和g++属于linux系的编译器,因此其提供的很多api函数都是对接linux的。无奈,只好下载了微软家的VS2019白嫖版(理论上来说visual c++之流应该也可以)。在调整了一些环境变量之后,用cl编译了一次,发现可以播放声音了。

2.2钢琴上每个键的声音从哪来/h3>

这个算是一个比较简单的问题,观察一下 上一些在线钢琴:

当时写这个爬虫是用来爬https://www.autopiano.cn/这个 站的(非常感谢autopiano.cn对本程序编写过程中提供的借鉴和启发意义,以及非常抱歉用爬虫消耗了一部分服务器资源,真心非常抱歉),但截至发稿时间该 站已更换了底层源码(应该是叫这个吧,打开 页和更换音色的时候服务器不会再发送一系列的MP3文件过来了,而是会发送一个js文件。具体的工作原理我也不是很懂,有待以后进一步探究。

2.3如何实现短时间内播放多个声音/h3>

钢琴上一个音可以持续3秒,甚至更多。但两个音之间的间隔远远小于3秒。如何在使用mciSendString函数播放声音的同时,使程序不停止在该函数处,而是继续运行并播放下一个音br> 答案是多线程。
但由于将之前的g++编译器换成了cl编译器,而微软的开发者工具包中没有pthread.h,手动引入pthread.h又比较复杂(太懒),所以本程序使用的是c++11引入的、功能远没有pthread.h丰富的、但是是由微软的开发者工具包中自带的、使用起来非常简便的、与windows系统有天然适应性(大概)的thread类。
(好像有个process.h也能多线程编程是用起来有点麻烦。本程序涉及的多线程方面的东西都比较浅显,所以是越简单越好)

2.4如何实现键盘按键与声音的对应/h3>

由于爬下来的声音文件名是对应的音名(当时是这样的),肯定不能直接输入音名来播放。方法之一是将每个音的文件名改成对应按键的名字,但这样手动操作量较大。我的想法是使用一个decode函数进行解码(,通过一定的规律将按键映射成相应的音名。

2.5如何实现按下对应键后立马播放对应的声音/h3>

平常使用的getchar函数、scanf函数以及cin方法等都需要按下回车后才能被程序所接收,这些函数显然不符合需求。经过 上查询后得知,conio.h中的getch函数和_getch函数有这样的效果。

2.6如何实现不断地接收输入并播放对应声音/h3>

死循环,while(True)…

2.7如何实现自动按输入的简谱演奏/h3>

可以设置将简谱写在一个文件中,播放时读取这个文件。原本的想法是直接将简谱的数字写在文件中,用*代表升8度,用.代表降8度,音符之间以空格间隔,一次用%s读取一个音符,然后再通过另一个decode函数解码为对应音名。但最终发现简谱实在过于复杂,应由人工将简谱上的音符转换为应该按下的按键,再直接将按键输入文件中,相当于是将stdin重定向为某一文件流之后的手动模式。

2.8如何存储人肉解码好的谱子/h3>

如图:

3.结构

首先这个程序初步确定有两个模式:自动和手动。两个模式的界面应该是不同的。所以根据结构化编程思想,一个函数负责一个模式,分别是automode()和manualmode()。既然有两个模式,那么肯定要有一个选择模式的开始界面,设为beginmode()。又开始必有结束,还要有一个endmode()。Mode之间的跳转可以放在main里,用一个int来存放跳到哪一个mode的信息。
所以main的伪代码大概为:(注意接下来都是伪代码

而beginmode应该是这样:

所以endmode:

而自动挡和手动挡比较复杂,应该更加细分的来讨论。
手动挡的话,首先最核心的应该是有一个播放声音的函数playsound(char)和decode()函数,这里我选择将decode()函数放在playsound里面,原因后面会讲。然后就是多线程。所以应该这么写:

自动挡的话,实际上是在手动挡的基础上改进的,因此实际上也差不多:

其余的函数较为细节,这里就不写了。

4.实现

略,理由是找不到当初的代码了。。。

5.分析

将所有模块们串联起来后,就有了一个界面不怎么精致,输入不怎么安全,但基本功能已经齐全的初版程序。当然,存在很多问题和可该进之处:

5.1按键的自动重复问题。

用过电脑的人都知道,按住一个键会打出来一连串的字符。在此程序中,表现为:按住一个键会连续触发多次播放。而现实中的钢琴显然不会发生这种情况, 页上的在线钢琴也不会。 上针对类似问题给出的解决方案是调用一个windows的关于键盘的api函数,但使用这个方案会使得手动模式下失去两键同时按下同时发音的能力,原因是其调用速度较慢,两次循环之间相当于sleep了一小段时间。经过深思熟虑之后,我发现按住键盘的问题可以通过弹琴的人来解决(“如果不能解决问题,就解决提出问题的人”的思想),而不能同时弹两个音,对于任意一个会弹钢琴的人来说,是不可容忍的。在两种情况中比较之后,我选择了不解决这个问题(待有缘人来解决这个问题)。

5.2音频播放时的第一个音频的延迟播放问题。

无论是自动挡还是手动挡,在播放第一个音的时候,都会停顿一小会,然后再播放,这样的话跟第二个音的间隔时间会很短很短,甚至重叠。在查看调试控制台的运行记录后,发现之前很久没有播放音频的第一次播放音频时,系统会加载一大堆相关.dll文件,而一段时间不播放音频后,系统会自动unload这些.dll文件。解决这个问题的方法之一是手动来让系统load/unload这些.dll文件,但缺点是比较复杂。所以我选择了一个比较愚蠢的办法:在自动挡和手动挡最后一次用户输入之后,播放一个25秒无声的MP3文件来强制加载相关dll,优点是操作简单,缺点是手动模式下闲置时间过长时,再次开始弹时还是会出现第一个音的延时问题,而且看起来很蠢,并没有从根本上解决问题。

5.3各个需要用户输入的地方的输入检测问题。

初版的程序对于用户的设想过于理想,未考虑用户不按规则输入的情况。解决方法开始时想的是用fflush(stdin),但发现好像不管用,输出一下发现是清除成功了的,但不知为何就是不行,最后还是用了while加getchar才解决。

5.4自动挡下短时间播放大量音频时曲速变慢问题。

原因应该是因为开的线程太多了,资源占用太大。但是要减少线程的话肯定是不行的,毕竟曲子一定要听完整的。所以我建议的解决方法是换一台机能更强大的电脑,但苦于资金有限而无法施行。

5.5曲速需要手动输入的问题。

众所周知每个曲子的曲速不尽相同,但曲谱文件中只有谱子这一信息,这样一来就必须手动输入曲速,这无疑会给不知道曲速的用户操作带来极大的不便。解决方法是在开头处整一个信息头,包含了曲名,曲速,版本等信息。

5.6自动播放途中无法操作的问题。

有时候听了一半不想听了,却不能退出,这样的设计实在是不人性化。解决方案是利用conio.h中的kbhit()函数检测有无键按下,没有的话不进入if分支防止曲速因此拖慢,有的话检测是不是回车,是的话直接跳出播放的循环。

当然,还有其他各种小问题,由于过于细节故此处不列出。

6.打磨

前面提到过,初版的界面极其简陋,虽然不要求什么高大上的UI设计,但至少需要一个用户可以看懂的界面。所以:

6.1给整个程序画一个框框。

让内容都在框框里显现,使其更像一个真正的游戏。当然这其实不算是一个小功能,或者说,开始时我觉得这似乎是一个小功能,原因是这应该算一个大功能。如何让内容在框框中显现,如何消除框框内的内容,如何排版,如何在调节显示的同时不影响同时正在演奏的音乐等等一个个问题都可以说是非常复杂了。

6.2在手动模式下画一个钢琴键盘图。

就是照着 页上的用字符画一个键盘,有什么难的原本是这么想的,但奈何这个键盘的图案实在是太复杂了,根本无法找出合适的规律来用循环打印出来,而 络上所教的一些方法(对于我来说)又过于复杂。最终只能非常愚蠢地直接将画好的图案打印上去。

6.3将曲速与曲子捆绑,保存在文件中。

上面已经提到过,具体是用一个结构体来直接保存信息头的所有信息。

6.4将目录下所有的曲子显示出来以供用户选择。

用system(“dir 路径”)可以方便地查看路径下的所有文件,在其中筛选.dat文件打印在屏幕上并标序 ,然后添加入声明好的字符串数组中,随后便可通过序 -1作为字符串数组的行下标来选择要播放的曲子。

6.5结尾处整一个制作人员名单。

单纯地打印出来的话其实挺简单,难就难在我希望能够像游戏或电影那样整一个滚动的字幕。这样一来可以使名单的长度不受框框大小限制,二来比较正式。最终实现方案是用字符串数组加上两层for循环。

6.6实现双音轨播放。

众所周知钢琴是用两只手弹的,但初版的自动挡只能在同一时间播放一个音,这样的话虽然也是能够演奏音乐,但实在是过于单薄,无法复现一些比较复杂的曲子。解决方法是将原先的自动挡的循环里面的大部分内容都提取出来,整合成一个函数playsong(),再额外写一个和弦的谱子,然后额外整一个文件流,然后每次循环开两个线程来播放两个音,并且将主线程调整为合并模式,即不运行完主线程不进行下一步操作,而另一个线程则设为分离模式。这样的话,每一次循环两个线程都相当于进行了一次强行同步,避免了单独开两个循环可能造成的两个音轨不同步的情况。

中途也遇到了一些其它的小问题,但过于细节此处不予以列出。

7.成品

双击exe文件,可打开一个初始界面:

此时可以根据对应音名的按键来弹钢琴了。
按回车可以返回主页面。然后输入2后按回车可以进入播放界面:

可以看见此时程序正在自动播放音乐。此时按下回车可以直接跳到音乐播放完的那一步:

8.反思

虽然程序是写了出来,而且能够良好运行,但与预期还是有较大差别:

  1. 未能实现像 页上的那些钢琴一样,手动模式下按下一个键时对应图形会发生变化,以表示按下。
  2. 未能实现像 页上的一样,自动播放时有一个音符雨的效果。且自动播放时只能在屏幕上显示主音轨的对应按键,无法显示另一个音轨的按键,不利于用户学习。
  3. 对于音轨的可拓展性较弱。双音轨对于钢琴来说是远远不够的,现实中的钢琴有的时候甚至会同时产生五六个音。
  4. 播放音频较为密集的时候会出现卡顿的情况,但此时CPU还是非常的空闲,可能是出现了所谓的“一核有难,七核围观”现象。如何充分的调动CPU分担计算任务,是一个非常值得思考的问题。
  5. 受电脑键盘所限,音域较为狭窄,也让熟悉钢琴的人弹起来不怎么习惯。
  6. 未能实现图形化界面,并不能称得上是一款真正的游戏。

因此,如果要改进的话,我认为可以从以下几个方面入手:

  1. 增加对midi文件的支持。当前计算机界已有一种主流的记谱文件格式,即midi。Midi文件中详细地记录了一支曲子中每个音的音高,出现的时间点,持续时间,声音大小,音色等信息。很明显,比我这个自己发明的记谱法不知道高到哪里去了。引入对midi文件的支持,不仅解决了自动模式中的音轨拓展性差问题,同时可以加宽自动模式下的音域,还可以实现对音长的精准控制,可谓是一举多得。同时 上还有许多现成的midi文件,可以直接下载播放,不像我这个一样,想演奏一首曲子时还要自己去 上找谱子,找到了还要自己一个个地手动输入到dat文件中,十分不便。
  2. 实现图形界面。将手动模式的界面变成动态的,按下一个键时对应的图标会有一定的动画效果,这样的视觉反馈可以给用户更好的使用体验。同时界面的跳转、自动模式下的暂停、播放、调整音量、快进、后退等都可以做成按钮,用鼠标来操控,降低了用户的操作难度。同时音符雨和滚动字幕的实现也会简单一些。
    #9.源码
/*    开始界面:手动输入or播放现有    将简谱人肉转码成对应键盘上的键的谱子,音符之间用空格隔开,使用fgetc读取单个音符    使用空格代表休止符    每读取一个音符,就新建一个线程并播放该音符.mp3,sleep一定的时间后--跟曲速有关--读取下一个音符    每个线程播放一定时间后自动结束*/#include       #include#include#include#include#include#includeusing namespace std;#pragma comment(lib,"winmm.lib") //[1]#define MAXLEN 127#define XSTART 16#define YSTART 5#define LENOFPAGE 146#define DEPTHOFPAGE 20short 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);//播放音乐void decoding_func(char keyboard_key,char *sound_name,short piano_type);//解码void gotoxy(int x,int y);//移动光标至指定位置void print_kuang();//打印框框void print_pkeys();//打印手动模式下的静态钢琴键盘void cls_kuang(short,short);//清除框框内指定行的信息HEAD readhead(FILE *);//读取谱子文件的信息头void HideCursor();//隐藏光标void ShowCursor();//显示光标char begin_page();//初始界面char exit_page();//退出界面char manual_page();//手动挡char auto_page();//自动挡int main(){    system("mode con cols=180 lines=38"); //[8]    short mode=0;    while(1){   //所有页面的中转站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 学校""

                                                        

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2020年1月7日
下一篇 2020年1月7日

相关推荐