看到Beep()就会想起上世纪90年代初在8086的机器或者稍后的286、386机器上用解释型Basic编简谱玩的情景,这便是那个声霸卡还没上市的年代里,几乎是人们在PC上唯一可编的声音了。
Beep的函数原型:
BOOL Beep( DWORD dwFreq; /*指定要发出的频率(HZ)*/ DWORD dwDuration; /*指定发音的时长,以毫秒为单位*/ );
通常先把do re mi ...的频率预定义好,再照着简谱把频率和时长编入两个数组或一个结构体数组。
#define do 523 #define re 578 #define mi 659 #define fa 698 #define so 784 #define la 880 #define si 988 ...... int freq[] = { do,re,mi,do,do,re,mi,do,...} int duration[] = {300,300,300,300,300,300,300,300,...}
最后用Beep()循环输出。
对此我做了些小改进:把频率定入一个结构体数组便于反复调用;简谱放入一个vector容器就不用管谱子的长短不用管数组的下标,只要按顺序push_back()每一个音符,还能随时插入删除某个音符,重复的小节可以用循环多次输出以缩短代码长度;简谱转代码时“所见即所得”看到什么数字就写入什么数字;音符的时长也以“拍”为单位,每个小节后加个空行便于检查每节的总时长。源代码如下:
#include <array> #include <vector> #include <iostream> #include <windows.h> using namespace std; #define PAI 300 //一拍的时长,可以自行调整 struct tone{ short a; short d; short g; //中音;低音;高音 short b; short e; short f; //半阶音 }; struct tune{ short t; //唱名 float l; //音长 short b; //音调 }; array<tone,8>m; short t(short a,short b) { switch(b){ case 0: return m[a].a; case 1: return m[a].d; case 2: return m[a].g; case 3: return m[a].b; case 4: return m[a].e; case 5: return m[a].f; } } short p(float p) { return (short)(PAI*p); } void initTone(void) { //把各音符的频率写入数组,一劳永逸可以随时调用 m.at(0)={0,0,0,0,0,0}; m.at(1)={523,262,1046,554,277,1109}; m.at(2)={578,294,1175,622,311,1245}; m.at(3)={659,330,1318,659,330,1318}; m.at(4)={698,349,1493,740,370,1556}; m.at(5)={784,392,1568,831,415,1661}; m.at(6)={880,440,1760,932,466,1865}; m.at(7)={988,494,1976,988,494,1976}; } void initTune(vector<tune>&s) { for (int i=0;i<2;i++){ s.push_back({1,1,0}); s.push_back({2,1,0}); s.push_back({3,1,0}); s.push_back({1,1,0}); } for (int i=0;i<2;i++){ s.push_back({3,1,0}); s.push_back({4,1,0}); s.push_back({5,2,0}); } for (int i=0;i<2;i++){ s.push_back({5,0.75,0}); s.push_back({6,0.25,0}); s.push_back({5,0.75,0}); s.push_back({4,0.25,0}); s.push_back({3,1,0}); s.push_back({1,1,0}); } for (int i=0;i<2;i++){ s.push_back({1,1,0}); s.push_back({5,1,1}); s.push_back({1,2,0}); } //以上根据简谱上的拍子和音调编入容器,方法如下: //第一个参数 1~7 对应do re mi fa so la si 0=休止符 //第二个参数 1拍=1;半拍=0.5 四分之一拍=0.25 以此类推 //第三个参数 一般就为0,低音=1 高音=2 对应的半阶音=3 4 5 //转简谱时看到什么数字就是什么,不用记频率数方便编辑和排错 } int main() { vector<tune>music; initTone(); initTune(music); cout<<"开始演奏《两只老虎》"<<endl; for (auto m:music) Beep(t(m.t,m.b),p(m.l)); return 0; }
背景音乐的实现
一个程序独占控制台CPU时间来演奏音乐,没有一点实用性。我们再来编一首《送别》并实现“后台演奏”,注意:碰到简谱中有重复的小节可多放几个子函数以供多次调用。
实际上,演奏的同时还要做其他工作,就是要创建多个线程来完成几个不同的工作:先调用<pthread.h>库函数pthread_create()创建一个线程来播放背景音乐,然后让主程序开始做其它工作,并且可以按需要来选择哪项任务先结束。源代码如下:
#include <array> #include <vector> #include <iostream> #include <ctime> #include <sstream> #include <windows.h> #include <pthread.h> using namespace std; #define PAI 400 //一拍的时长,可以自行调整 struct tone{ short a; short d; short g; //中音;低音;高音 short b; short e; short f; //半阶音 }; struct tune{ short t; //唱名 float l; //音长 short b; //音调 }; array<tone,8>m; short t(short a,short b) { switch(b){ case 0: return m[a].a; case 1: return m[a].d; case 2: return m[a].g; case 3: return m[a].b; case 4: return m[a].e; case 5: return m[a].f; } } short p(float p) { return (short)(PAI*p); } void initTone(void) { m.at(0)={0,0,0,0,0,0}; m.at(1)={523,262,1046,554,277,1109}; m.at(2)={578,294,1175,622,311,1245}; m.at(3)={659,330,1318,659,330,1318}; m.at(4)={698,349,1493,740,370,1556}; m.at(5)={784,392,1568,831,415,1661}; m.at(6)={880,440,1760,932,466,1865}; m.at(7)={988,494,1976,988,494,1976}; } void repeat1(vector<tune>&s) { s.push_back({5,1,0}); s.push_back({3,0.5,0}); s.push_back({5,0.5,0}); } void repeat2(vector<tune>&s) { s.push_back({6,1,0}); s.push_back({1,1,2}); s.push_back({5,2,0}); } void repeat3(vector<tune>&s, short i) { s.push_back({i,2,0}); s.push_back({0,1,0}); s.push_back({0,1,0}); } void initTune(vector<tune>&s) { for (int i=0;i<2;i++){ repeat1(s); s.push_back({1,2,2}); repeat2(s); s.push_back({5,1,0}); s.push_back({1,0.5,0}); s.push_back({2,0.5,0}); s.push_back({3,1,0}); s.push_back({2,0.5,0}); s.push_back({1,0.5,0}); repeat3(s,2); repeat1(s); s.push_back({1,1.5,2}); s.push_back({7,0.5,0}); repeat2(s); s.push_back({5,1,0}); s.push_back({2,0.5,0}); s.push_back({3,0.5,0}); s.push_back({4,1.5,0}); s.push_back({7,0.5,1}); repeat3(s,1); s.push_back({6,1,0}); s.push_back({1,1,2}); s.push_back({1,2,2}); s.push_back({7,0.5,0}); s.push_back({6,0.5,0}); s.push_back({7,0.5,0}); s.push_back({1,2,2}); s.push_back({6,0.5,0}); s.push_back({7,0.5,0}); s.push_back({1,0.5,2}); s.push_back({6,0.5,0}); s.push_back({6,0.5,0}); s.push_back({5,0.5,0}); s.push_back({3,0.5,0}); s.push_back({1,0.5,0}); repeat3(s,2); repeat1(s); s.push_back({1,1.5,2}); s.push_back({7,0.5,0}); repeat2(s); s.push_back({5,1,0}); s.push_back({2,0.5,0}); s.push_back({3,0.5,0}); s.push_back({4,1.5,0}); s.push_back({7,0.5,1}); repeat3(s,1); } } void gotoXY(short x, short y) { COORD position = {x, y}; HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(hConsole, position); } bool play_not_end=true; //设置演奏标记 void* play(void* args) { vector<tune>music; initTone(); initTune(music); gotoXY(28,8); cout<<"背景音乐:《送别》"; for (auto v:music) if (play_not_end) Beep(t(v.t,v.b),p(v.l)); else break; play_not_end=false; } char strTime[20]; void* coutTime(void* args) { time_t lt; while (play_not_end){ time(<); strftime(strTime,sizeof(strTime),"%Y-%m-%d %H:%M:%S",localtime(<)); Sleep(300); } play_not_end=false; } int main() { pthread_t pt; int ret = pthread_create(&pt, NULL, play, NULL); //创建一个线程,play()变相成为背景音乐 if (ret!=0) cout<<"create thread_1 error: error_code="<<ret<<endl; ret = pthread_create(&pt, NULL, coutTime, NULL); //再创建一个线程,用于显示当前系统时间 if (ret!=0) cout<<"create thread_2 error: error_code="<<ret<<endl; //在 pthread_create()与 pthread_exit()之间,便是程序工作的主场 Sleep(100); gotoXY(28,10); cout<<"工作开始:循环次数 "; for(int i=1;play_not_end;i++){ gotoXY(48,10); cout<<i<<endl; gotoXY(60,0); cout<<strTime; //显示另外一个线程取回的时间 Sleep(300); if (i==100) play_not_end=false; //工作在音乐演奏完之前结束,只要把标记设为false即可 } gotoXY(28,12); cout<<"工作结束!"<<endl; pthread_exit(NULL); return 0; }
注意:本程序在Dev-C++ 5.11/编译器TDM-GCC 4.9.2 64-bit 上通过编译,若用VS编译找不到头文件<pthread.h>,请搜索“如何在vs2017上使用pthread.h”并自行安装库文件。
附:《两只老虎》、《送别》简谱