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

0.缘由

C语言课期末大作业。由于是开放性的作业,随便写着玩的,就写了这么一个玩意。虽然高中的时候接触过一些音乐或者音频软件,像Au,FLstudio,Minecraft之类的,但实际上对音乐方面的还算是一窍不通,其原因是不同于许多同龄人,在小的时候并没有被父母逼着去学一门乐器,也就没有接受过正规的乐理方面的教育。如果本文或文中代码里出现一些低级的音乐常识方面的错误,还请不吝赐教。当然,作为半年前刚刚接触代码的大一新生,代码部分存在很多漏洞,不规范之处和可优化的地方,也希望能够给予谅解并多多指教。

1.环境

Win10系统

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

VS2019白嫖版附赠的cl编译器,为什么用这个后文会讲

Visual Studio Code编辑器,coderunner插件,C/C++拓展插件

vscode里的tasks.json文件。不要问我任务名为什么既是build又是test,问就是脑子抽了

vscode里的launch.json文件,此次不需要用到下面那个g++

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

链接的库:winmm.lib

计算机配置:

家贫,买不起好机子

2.思路

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

2.1如何在程序内播放声音?

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

2.2钢琴上每个键的声音从哪来?

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

图片原网址:https://virtualpiano.net/

发现在打开网页的时候服务器会发送一系列MP3文件过来。不用说,就是每个键对应的音。那么接下来就好办了,遂设计爬虫爬取之:

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")

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

2.3如何实现短时间内播放多个声音?

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

2.4如何实现键盘按键与声音的对应?

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

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

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

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

死循环,while(True)…

2.7如何实现自动按输入的简谱演奏?

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

2.8如何存储人肉解码好的谱子?

如图:
图为在vscode中打开的样子(乐曲为平凡之路)
对照一下原乐谱:
来源:http://www.doc88.com/p-7794828256780.html

3.结构

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

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 //这个地方好像用switch语句也可以
    System.cls;
    Return 0;
}

而beginmode应该是这样:

Int beginmode(){
    Cout << 一些开始的字 << endl;
    Cout << 选择你的模式:1.手动 2.自动 3.退出 << endl;
    Int 选择=0;
    Scanf(“%d”,&选择);
    检测输入,不符合的重新输入;
    Sys.cls;
    Return 选择;
}

所以endmode:

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

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

Int manualmode(){
    Print(一些信息);
    声明一些变量;
While(true):
        字符变量 = getch();
        If(是回车):
            Break;
        创建线程并运行playsound;/*此处传入变量较为麻烦,因此直接
        将字符变量传入,使解码在playsound中进行*/
        分离线程;//不分离的话程序会阻塞于此处,直至音频播放完毕
    Print(退出);
    Sys.cls;
    Return 0;
}

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

Int automode(){
    FILE *f;
    选择打开的文件;
    打开文件流;
    While(true):
        字符变量 = fgetc();
        Print(字符变量);
        If 字符变量是一些谱子上用于标记的,与演奏无关的字符:
            Continue;
        创建线程并运行playsound;
        分离线程;
        Sleep(一定秒数,根据曲速而定);
    Print(一些信息,告诉用户结束了);
    Sys.pause;
    Sys.cls;
    Return 0;
}

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

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文件,可打开一个初始界面:
初始界面,有点简陋

输入1,按回车后进入手动模式:
手动挡,画面是静态的,十分简陋

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

此时可以选择需要播放的音乐。(注:single_ver为单音轨版)此处我们选择9号音乐《平凡之路》:
播放中....

可以看见此时程序正在自动播放音乐。此时按下回车可以直接跳到音乐播放完的那一步:
放完了,按任意键回到主页面

按下任意键后可以回到主页面。输入3按回车可以观看制作人员名单。
看完后按任意键可退出程序:(播放中途按任意键可加速播放)
语出自——头号玩家,哈利迪

8.反思

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

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

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

  1. 增加对midi文件的支持。当前计算机界已有一种主流的记谱文件格式,即midi。Midi文件中详细地记录了一支曲子中每个音的音高,出现的时间点,持续时间,声音大小,音色等信息。很明显,比我这个自己发明的记谱法不知道高到哪里去了。引入对midi文件的支持,不仅解决了自动模式中的音轨拓展性差问题,同时可以加宽自动模式下的音域,还可以实现对音长的精准控制,可谓是一举多得。同时网上还有许多现成的midi文件,可以直接下载播放,不像我这个一样,想演奏一首曲子时还要自己去网上找谱子,找到了还要自己一个个地手动输入到dat文件中,十分不便。
  2. 实现图形界面。将手动模式的界面变成动态的,按下一个键时对应的图标会有一定的动画效果,这样的视觉反馈可以给用户更好的使用体验。同时界面的跳转、自动模式下的暂停、播放、调整音量、快进、后退等都可以做成按钮,用鼠标来操控,降低了用户的操作难度。同时音符雨和滚动字幕的实现也会简单一些。
    #9.源码
/*
    开始界面:手动输入or播放现有
    将简谱人肉转码成对应键盘上的键的谱子,音符之间用空格隔开,使用fgetc读取单个音符
    使用空格代表休止符
    每读取一个音符,就新建一个线程并播放该音符.mp3,sleep一定的时间后--跟曲速有关--读取下一个音符
    每个线程播放一定时间后自动结束
*/
#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);//播放音乐
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 学校",\
        "MUSIC RESOURCE",\
        "autopiano.cn",\
        "SPECIAL THANKS TO",\
        "Professor 老师",\
        "T.A. 助教1",\
        "T.A. 助教2",\
        "室友",\
        "github.com/WarpPrism/AutoPiano",\
        "runoob.com/cprogramming/c-tutorial.html",\
        "",\/*最后一段字符串由于没有下一段字符串来清掉它,会滞留在屏幕上,因此用空字符串来做最后一个*/
        "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;   //快速跳过制作人员名单
            }
            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);  //滚动字幕的效果
    }
    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("请按任意键继续. . .")/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);   //将光标移到方框的正中央并打印文字
    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);   //将光标移至中央
    char mode[127]={0};
    while(1){
        cin >> mode;     //输入检测
        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){ //文件过多时换个行继续
                xstart += lenofsongname+5;
                ystart = YSTART+2;
                lenofsongname = 0;
                gotoxy(xstart,ystart);
            }
        }
    }
    fclose(dir);
    CwithI tempci;
    while(1){
        scanf("%9s",tempci.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,'+');   //播放一段无声的MP3以加载播放器相关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){//有和弦时进入此分支
        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){//其中一个谱子放完了自动跳出整个循环
                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,'+');   //播放一段无声的MP3以加载播放器相关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函数中被引用,可有效降低创建线程时传参的复杂程度
    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'}; //所有音名对应的按键
    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"};  //音名数字部分
    
    for(short i=0;i<sizeof(jian);i++){
        if(keyboard_key==jian[i]){
            short zimu_bianhao=i%12;   //字母部分12个一轮回
            short shuzi_bianhao=i/12;    //数字部分每十二个音+1
            char ss_zimu[5],ss_shuzi[2];
            strcpy(ss_zimu,zhi_zimu[zimu_bianhao]);
            strcpy(ss_shuzi,zhi_shuzi[shuzi_bianhao+piano_type]);  //带一个piano_type补正可以拓展音域
            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");    //播放一段无声的声音,用于加载与播放器有关的DLL文件
    }
    else{
        decoding_func(keyboard_key,sound_name,piano_type);   //将键盘上的键对应地解码成音名
    }
    sprintf(temp_command,"open piano\\%s.mp3 alias %s",sound_name,sound_name);
    mciSendStringA(temp_command,0,0,0); //[5] //打开音名.mp3
    sprintf(temp_command,"play %s",sound_name);
    mciSendStringA(temp_command,0,0,0);    //播放
    Sleep(10000);
    sprintf(temp_command,"close %s",sound_name);   //关闭
    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'){//遇到|和换行符时,由于不对乐曲本身发挥任何作用,因此需要特殊处理
        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);//此处控制曲速,注意:与一般音乐软件使用的曲速单位bpm不同,这里只是单纯的停顿一定毫秒
    }
    if(kbhit()){
        char kbout='\0';
        kbout = _getch();
        if(kbout=='\r'){ //播放中途按回车键退出
            *flag = 2;
            return;
        }
    }
    thread t(play_sound,keyboard_key);  //单独分出线程来播放声音
    t.detach();    
    return;
}

void gotoxy(int x,int y){    //[6]
    _COORD pos;
    pos.X=x;
    pos.Y=y;
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),pos);  //设置鼠标位置
}

void print_kuang(){
    short start_x=XSTART,start_y=YSTART;
    short page_x=LENOFPAGE,page_y=DEPTHOFPAGE;
    gotoxy(start_x,start_y);      //将鼠标移动到起始点
    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个=
            }
        }
        else{
            gotoxy(start_x,start_y+y);
            cout << '|';
            gotoxy(start_x+page_x,start_y+y);
            cout << '|';      //每行开头和末尾打印|
        }
    }
}

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);//清除框框内的文字,比system(“cls”)更快
    for(short c=0;c<LENOFPAGE-1;c++){
        printf(" ");
    }
    gotoxy(XSTART+1,line+mode);//清除当前行数之外的另一行
    for(short c=0;c<LENOFPAGE-1;c++){
        printf(" ");
    }
    gotoxy(XSTART+1,line);//光标回到初始位置
}

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

void ShowCursor(){
	CONSOLE_CURSOR_INFO cursor;    //显示光标
	cursor.bVisible = TRUE;    
	cursor.dwSize = sizeof(cursor);    
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);    
	SetConsoleCursorInfo(handle, &cursor);
}

HEAD readhead(FILE *puzi){  //读取曲谱文件的标头,获取曲速,曲名等信息
    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兼具计数和条件判断的功能
            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);   //将信息头最后一个换行符给吃掉
                break;
            }
            memset(tempstr,'\0',sizeof(tempstr));
        }
        if(flag%2==1){
            tempstr[count++] = keyboard_key;
        }
    }
    return temphead;
}


/*
参考文献:
[1]在C语言控制台程序中播放MP3音乐,https://www.cnblogs.com/honkly/p/3738022.html,2019-12-01.
[2]C++之 _getch()和getchar()的区别,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]怎样清空键盘缓冲区?,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++得到光标坐标和移动光标,https://www.cnblogs.com/noevil/archive/2010/10/12/1849092.html,2019-12-06.
[7]C++隐藏光标,https://blog.csdn.net/qq_41222732/article/details/97145818,2019-12-12.
[8]C语言如何控制控制台窗口大小,https://blog.csdn.net/ZouHuiDong/article/details/89812472,2019-12-19.
*/

命名和注释之类的不是很规范,一些循环也使用了比较笨的处理方法,献丑了
附上平凡之路的谱子:(保存为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.结语

写到这里才发现写了好多,真的会有人完整地看完吗。。。。。。

发布了2 篇原创文章 · 获赞 3 · 访问量 75

猜你喜欢

转载自blog.csdn.net/qq_45433084/article/details/103898227