这是我们小学期的第一个大作业,因感受颇深,特此写下这篇博客留作纪念。
内容:设计一个带有播放控制和音量调节功能的控制台音乐播放器,其中播放控制的子菜单能实现播放暂停切换、停止当前曲、播放上一曲和下一曲的功能。在进入主菜单前能遍历工程文件内所有文件夹并在屏幕上显示所有mp3扩展名的音乐文件。
查阅相关资料:
1.sprintf(wsprintf函数的使用)
(1)sprintf
函数功能:把格式化的数据写入某个字符串
函数原型:intsprintf( char *buffer, const char *format [, argument] … );
返回值:字符串长度(strlen)
eg.
char* who = "I";
char* whom = "CSDN";
sprintf(s, "%s love %s.", who,whom); //产生:"I love CSDN. " 这字符串写到s中
sprintf(s, "%10.3f", 3.1415626); //产生:" 3.142"
(2)wsprintf
函数功能:将一系列的字符和数值输入到缓冲区。输出缓冲区里的的值取决于格式说明符(即"%")。如果写入的是文字,此函数给写入的文字的末尾追加一个'\0'。
返回值:写入的长度,但不包括最后的'\0'。
函数原型:intwsprintf(LPTSTR lpOut,//输出缓冲区,最大为1024字节
LPCTSTR lpFmt, //格式字符串
...//需输出参数列表)//这个函数的参数个数无法确定
2.mciSendString
mciSendString是用来播放多媒体文件的API指令,可以播放MPEG,AVI,WAV,MP3等多媒体文件。使用时应包含头文件
3.VS2010等较高版本的visual studio编译器的Unicode字符编码占用了2个字节,这和VC6.0占一个字节不同,可能会导致控制台程序无法播放音乐。
4.编译器IDE适配系统方面,VC6.0中文版无法在win10系统上运行,在网上下了一个英文包覆盖原来的汉化包即可。
小结
在老师带我们编码和优化代码结构的过程中,我体会到了如何用C语言几个基本的语法点搭建一个实用的软件,这是学了大一一学年后的第一个贴近应用的实战项目。
在4天的分阶段分模块编码过程中,也认识到C语言的数组、指针、函数等内容如何与工程实践结合,优化架构,降低不同模块的耦合度,利用编译器的纠错特性,提高代码的健壮性和容错性。
1.简单软件的菜单选择常使用条件语句,若菜单选项过多switch比if-else语句效率更高(switch-case语句直达目标,if-else语句按顺序逐个检索。但是switch的入口变量只能是整型数据或者单个字符,这是它的局限性);
2.模块化编程常使用头文件分割的方法,并且一个头文件中一般写这个文件对外提供的接口(别的文件要使用的变量和函数声明);
3.在软件开发过程中,常需要限定控制变量和函数的作用域和生存期。C语言中与之相关的两个关键字为static和extern。在多个菜单需要共享一个变量或者共同使用一种状态时常使用静态局部变量,因为静态局部变量未明确赋值时编译会自动赋初值0,且在下次定义的函数内时会保留上一次运行的值,而自动变量未赋初值时是一个不确定的值,不会保留且每次进入函数都要申请释放空间,无论是逻辑的正确性,效率性还是容错性都更好,比如在PlayControlMenu()函数内的播放状态枚举变量即设为static,因其为播放、暂停、停止三个功能所共用且共同影响其变更。
extern的全局变量在工程中尽量少用,因为这样会降低代码的封装性,不用extern的方法通常是借鉴C++类的成员访问权限的思想,对不能被外界轻易更改的数据设置getXX函数接口。(例如这份工程代码最值得称道的是在后期分模块的过程中把所有的与播放列表有关的数组、变量和原本在PlayInterface.c中PlayControlMenu函数里有关列表转换的代码段提炼出基本操作编写getXX函数,并独立在PlayList.c文件封装起来,也避免了曲号要为Mp3Player.c,PlayList.c两个文件共用而不得不设置全局变量的情况。)
4.其他几个小的注意点:
(1)对于循环判断条件中的变量,在工程实践中通常在循环外初始化为-1,这是利用-1在内存中补码的二进制形式全为1的特性,最大限度避免不可预知的逻辑错误。
(2)scanf要在按下回车后才能将暂存在缓冲区中的字符发送,可以用getch函数,这样在按下按键后就能立刻发送指令,但是要注意getch()的原理是等待你按下任意键之后,把该键字符所对应的ASCII码赋值给变量,因而对于int型左值变量要注意隐式类型转换
(eg.执行编号1对应的菜单栏选项,int input; input = getch()-48;// “1”->49)
(3)可用
自己出现的问题:
1.在开始编写下一曲上一曲转换的功能时想当然一个菜单对应一个函数,于是在PlayInterface.c内重新编写了LastSong(),NextSong()两个函数。在编程过程中意识到它们可以拆解为两个更基本的步骤:停止当前曲播放的进程、打开下一曲扩展名为mp3的文件并向MCI库发送播放指令。但是也没有调用之前功能已经写的函数而是直接复制之前函数的代码段添加修改。这样做的结果是虽然能勉强实现下一曲和上一曲的切换,但存在两个问题:
(1)虽然自己的编程思路正确,但是在按下“3”或者“4”键后不能直接播放下一曲或上一曲,还必须按1次“1”(播放/暂停切换键)才会响起下一曲;
(2)于是将错就错,看看还会有什么其他异常状况,把播放状态调到暂停,依次按313131(播放列表只有3首歌)也就是列表循环一遍,发现第一首歌会在之前暂停的地方继续播放,再继续下去发现所有的歌都是这种情况,表明所有歌在切换到下一曲后没有结束进程,不符合实际需求。
把代码修改,重用stop(),play(), pause()函数后解决了上述问题。
这说明要合理利用代码的重用性提炼相同任务。不必担心调用多层嵌套函数而影响效率。
实际上这正是排除代码冗余,完善架构,提高程序执行效率的一种方式。
2.使用清屏命令行cls也存在一定的副作用,即一些提示性信息比如当前播放曲目在屏幕上转瞬即逝,我在C++ PRIMER中查到了编写while延时循环的方法。原理是利用系统时钟脉冲,将
代码如下:
//利用系统时钟,逐条显示文件夹内包含的mp3歌曲文件名
clock_tdelay=2.0*CLOCKS_PER_SEC;//转换为系统时钟脉冲数
clock_tstart=clock();
while(clock()-start
;
printf("%s\n",FindFileData.cFileName);
//FindFileData.cFileName是文件名
这个播放器还有两点可以提高的地方,首先不能动态添加删除列表,而数组在增删改方面的效率低,因此实现这个功能要用到新的数据结构链表; 此外不能在一首歌播完后自动播放下一曲,而这个播放器一直用的是getch()函数进行菜单选择,程序始终处于等待输入的状态,进程被挤占,无法监测一首曲子是否播完,因而要用到更为复杂的多线程编程,这个留待以后解决。
总之,做这样一个项目从思维,调试,自主发现和解决问题的能力和学习的主动性全方位锻炼了我的软件编码实践能力。
下面上代码
main.c
#include"Mp3Player.h"
#include
int main()
{
find(".\\music");
MainMenu();
return 0;
}
Mp3Player.h
//头文件中写这个文件对外提供的接口
extern void MainMenu();
Mp3Player.c
#include
#include
#include
#include
#include"PlayInterface.h"
#include"PlayList.h"
#include"ShowDelay.h"
/*getch():
所在头文件:conio.h
函数用途:从控制台读取一个字符,但不显示在屏幕上
(用getch();会等待你按下任意键,再继续执行下面的语句;
用ch=getch();会等待你按下任意键之后,把该键字符所对应的ASCII码赋给ch,再执行下面的语句。)
函数原型:intgetch(void)
返回值:读取的字符
对比:getchar()函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1。
*/
//#define STATUS_STOP 0
//#define STATUS_PLAY 1
//#define STATUS_PAUSE 2
enum STATUS{
STATUS_STOP=0,
STATUS_PLAY,
STATUS_PAUSE
};//用枚举变量限制状态的变化范围
//函数声明
static void PlayControlMenu();//static限定作用域在本文件内部
static void SoundControlMenu();
void MainMenu();
void PlayControlMenu()
{
char *name="Hero.mp3";
//char name1[]="Hero.mp3";
//char *names[10]={"Hero.mp3","Baby.mp3"};存的是10首歌曲的首地址
int input2 = -1;
static enum STATUS status = STATUS_STOP;//用枚举变量status记录播放状态,0,1,2分别代表停止,播放,暂停(用了静态局部变量)
while (input2 != 0)
{
printf("第%d曲 -- %s\n", getCurNo() + 1, getName());
showdelay(0.5);
system("cls");
printf("1.播放/暂停\n");
printf("2.停止\n");
printf("3.下一曲\n");
printf("4.上一曲\n");
printf("0.退出\n");
input2=_getch()-48;//“1”->49
switch(input2)
{
case 1:printf("1.播放/暂停\n");
//播放或暂停音乐
if(status!=STATUS_PLAY)
{
printf("播放\n");
showdelay(0.3);
play(getName());
status=STATUS_PLAY;
}
else
{
printf("暂停\n");
showdelay(0.3);
pause(getName());
status=STATUS_PAUSE;
}
break;
case 2:
printf("2.停止\n");
stop(getName()); //停止当前音乐
status = STATUS_STOP;
showdelay(0.3);
break;
case 3:printf("3.下一曲\n");
stop(getName());
curNoDown();
play(getName());
break;
case 4:printf("4.上一曲\n");
stop(getName());
curNoUp();
play(getName());
break;
case 0:printf("0.退出\n");
break;
default:
printf("您输错了\n");
break;
}
}
}
void SoundControlMenu()
{
int input3 = -1;
while (input3 != 0)
{
system("cls");
printf("第%d曲 -- %s\n", getCurNo() + 1, getName());
printf("1.音量增大\n");
printf("2.音量减小\n");
printf("0.退出\n");
input3=_getch()-48;
switch(input3)
{
case 1:printf("1.音量增大\n");
VolumnUp(getName());
break;
case 2:printf("2.音量减小\n");
VolumnDown(getName());
break;
case 0:printf("0.退出\n");
break;
default:
printf("您输错了\n");
break;
}
}
}
void MainMenu()
{
//一般程序开发过程中初始化为-1,因为-1在内存中存的是补码,而负数的补码为原码取反加1,
//eg.1在内存中二进制形式00000001,它的反码是11111110,补码11111111,即为-1在内存中的形式为11111111.
//因为input是int型,4字节,所以内存中是32个1,即0XFFFFFFFF。
int input = -1;
//int input=0;
while(input != 0)
{
system("cls");
printf("1.播放控制\n");
printf("2.音量调节\n");
printf("0.退出\n");
//scanf("%d",&input);
input=_getch();
input-=48;
switch(input)
{
case 1:printf("1.播放控制\n");
//二级菜单
PlayControlMenu();
break;
case 2:printf("2.音量调节\n");
//二级菜单
SoundControlMenu();
break;
//default:
case 0:printf("0.退出\n");
break;
default:
printf("您输错了\n");
break;
}
}
}
PlayInterface.h
extern void play(const char *name); //播放音乐
extern void pause(const char *name); // 暂停播放
extern void stop(const char *name); //停止播放
extern void VolumnUp(const char *name); //调大音量
extern void VolumnDown(const char *name); //调小音量
PlayerInterface.c
#include
#include
#include // 包含getch()声明的头文件
#include
#include
#include // mci库头文件
#pragma comment(lib, "winmm.lib") // 链接/指定MCI库,mciSendString函数的定义在winmm.lib中
#define MAX_SONG 3
//extern int serial_num;
//extern enum STATUS status;
// 播放当前曲
void play(const char *name) //播放音乐
{
char cmd[MAX_PATH] = {0};
char pathname[MAX_PATH] = {0};
// 加路径
//intprintf(const char*);
//intsprintf(char* ,const char*,...);
wsprintf(pathname, ".\\music\\%s", name);
// GetShortPathName用来转换短名,要求被转换的歌名必须能在指定目录下找到文件,否则转换失败。
// 第一个参数:源文件名,第二个参数:目的文件名,第三个参数:目的数组长度。
//源文件?目的文件?区别?
GetShortPathName(pathname, pathname, MAX_PATH);//滤掉文件路径的空格
// 定义发往MCI的命令,cmd指定命令存储的数组,后面参数跟printf()相同
wsprintf(cmd, "open %s", pathname);
// 发送命令。
// 一、存储命令的数组首地址,二、接受MCI返回的信息,三、接受数组的长度,四、没用,NULL
mciSendString(cmd, "", 0, NULL);
wsprintf(cmd, "play %s", pathname);
mciSendString(cmd, "", 0, NULL);
}
// 暂停当前曲,曲号由curno记录
void pause(const char *name) // 暂停播放
{
char cmd[MAX_PATH] = {0};
char pathname[MAX_PATH] = {0};
// 加路径
wsprintf(pathname, ".\\music\\%s", name);
// GetShortPathName用来转换短名,要求被转换的歌名必须能在指定目录下找到文件,否则转换失败。
// 第一个参数:源文件名,第二个参数:目的文件名,第三个参数:目的数组长度。
GetShortPathName(pathname, pathname, MAX_PATH);
wsprintf(cmd, "pause %s", pathname);
mciSendString(cmd,"",0,NULL);
}
// 停止当前曲,曲号由curno记录
void stop(const char *name)
{
char cmd[MAX_PATH] = {0};
char pathname[MAX_PATH] = {0};
// 加路径
wsprintf(pathname, ".\\music\\%s", name);
// GetShortPathName用来转换短名,要求被转换的歌名必须能在指定目录下找到文件,否则转换失败。
// 第一个参数:源文件名,第二个参数:目的文件名,第三个参数:目的数组长度。
GetShortPathName(pathname, pathname, MAX_PATH);
wsprintf(cmd, "stop %s", pathname);
mciSendString(cmd,"",0,NULL);
wsprintf(cmd, "close %s", pathname);
mciSendString(cmd,"",0,NULL);
}
/*
void nextSong(char *name)
{
char cmd[MAX_PATH] = {0};
char pathname[MAX_PATH] = {0};
//停止播放当前歌曲
wsprintf(pathname, ".\\music\\%s", name);
GetShortPathName(pathname, pathname, MAX_PATH);
wsprintf(cmd, "stop %s", pathname);
mciSendString(cmd,"",0,NULL);
//播放下一首歌曲
wsprintf(pathname, ".\\music\\%s", name+1);
GetShortPathName(pathname, pathname, MAX_PATH);
wsprintf(cmd, "open %s", pathname);
mciSendString(cmd, "", 0, NULL);
wsprintf(cmd, "play %s", pathname);
mciSendString(cmd, "", 0, NULL);
//歌曲序号+1,注意列表循环播放,最后一曲的下一曲为第一曲
if(serial_num0)
serial_num--;
else
serial_num=MAX_SONG-1;
}
*/
void VolumnUp(const char *name)
{
char path[MAX_PATH] = {0};
char cmd[MAX_PATH] = {0};
char res[MAX_PATH] = {0};
int volumn = 0;
// 加路径
sprintf(path, ".\\music\\%s", name);
// GetShortPathName用来转换短名,要求被转换的歌名必须能在指定目录下找到文件,否则转换失败。
// 第一个参数:源文件名,第二个参数:目的文件名,第三个参数:目的数组长度。
GetShortPathName(path, path, MAX_PATH);
sprintf(cmd, "status %s volume", path);
mciSendString(cmd, res, MAX_PATH, NULL);
// 音量转换为整形,并作加法操作
volumn = atoi(res);
volumn += 100;
// 拼接设置音量命令
sprintf(cmd, "setaudio %s volume to %d", path, volumn);
// 发送设置音量命令
mciSendString(cmd, "", 0, NULL);
}
void VolumnDown(const char *name)
{
char path[MAX_PATH] = {0};
char cmd[MAX_PATH] = {0};
char res[MAX_PATH] = {0};
int volumn = 0;
// 加路径
sprintf(path, ".\\music\\%s", name);
// GetShortPathName用来转换短名,要求被转换的歌名必须能在指定目录下找到文件,否则转换失败。
// 第一个参数:源文件名,第二个参数:目的文件名,第三个参数:目的数组长度。
GetShortPathName(path, path, MAX_PATH);
sprintf(cmd, "status %s volume", path);
mciSendString(cmd, res, MAX_PATH, NULL);
// res是个字符数组,不能实现减法操作,所以用atoi()转换为整形
volumn = atoi(res);
volumn -= 100;
// 将变更后的音量发送给MCI进行设定
sprintf(cmd, "setaudio %s volume to %d", path, volumn);
mciSendString(cmd, "", 0, NULL);
}
PlayList.h
extern void find(char * lpPath); //在指定文件夹内遍历查找mp3文件
extern char *getName(); //获取歌名
extern int getCurNo(); //获得曲号
extern void curNoDown(); //下一曲,曲号+1
extern void curNoUp(); //上一曲,曲号-1
PlayList.c
#include
#include"ShowDelay.h"
#define MAX_SONG 10
static char names[MAX_SONG][100]={"Beautiful Times.mp3","Fireflies.mp3","Hero.mp3",};//存的是10首歌曲的歌曲名
static int serial_num = 0;//歌曲序号,也即歌曲名字符串数组的下标
static int total = 0; //总曲数
char *getName()
{
return names[serial_num];
}
int getCurNo()
{
return serial_num;
}
void curNoDown() //变为下一首的曲号
{
serial_num++;
if(serial_num==3)
serial_num=0;
}
void curNoUp() //变为上一首的曲号
{
serial_num--;
if(serial_num==-1)
serial_num=2;
}
// 遍历文件夹,利用windows API
void find(char * lpPath)
{
char szFind[MAX_PATH],szFile[MAX_PATH];
WIN32_FIND_DATA FindFileData;
HANDLE hFind;
strcpy(szFind,lpPath);
strcat(szFind,"\\*.mp3");
hFind = FindFirstFile(szFind,&FindFileData);
if(INVALID_HANDLE_VALUE == hFind)
return;
while(TRUE)
{
if(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
if(FindFileData.cFileName[0]!='.')
{
strcpy(szFile,lpPath);
strcat(szFile,"\\");
strcat(szFile,FindFileData.cFileName);
find(szFile);
}
}
else
{
showdelay(1.5);
printf("%s\n",FindFileData.cFileName);
// FindFileData.cFileName是文件名
//二维数组行地址 &names[1][0]/names[1]/(name+1)
strcpy(names[total],FindFileData.cFileName);
total++;
}
if(!FindNextFile(hFind,&FindFileData))
break;
}
}
ShowDelay.h
extern void showdelay(float sec);
ShowDelay.c
#include
void showdelay(float sec)
{
clock_t delay=sec*CLOCKS_PER_SEC;//转换为系统时钟脉冲数
clock_t start=clock();
while(clock()-start