学习笔记-C语言关于waveout接口的使用(终?)

学习笔记-C语言关于waveout接口的使用(终?)

好,介绍下这段时间写的东西,从何讲起呢?

从最直观的开始吧,文件数量,对文件数量,这个项目共有5个文件,分别是

  • WaveOut.c
  • file_info.h
  • onWindows.h
  • file_info.c
  • onWindows.c

这五个文件各有神通,支撑起了本项目主要的两个功能,一个是将任意位深的音乐以16bit播放,另一个是将任意位深的音乐以16bit文件的形式输出。头文件中装的是对C文件的函数声明,没什么好讲的,就先从主函数所在的文件WaveOut.c开始介绍吧

1 WaveOut.c

WaveOut.c中主要包含了主函数main和监听作用的线程函数Listening。其实一开始是打算在这个文件中增加个中间层,为后续移植做准备,但由于完全没这方面的概念,就不了了之了,但还是保留了一部分内容,所以WaveOut.c开头为:

#ifdef _WIN32
    #include <windows.h>		// Windows平台的头文件和定义
    #include <mmsystem.h>
    #include "onWindows.h"
    #pragma comment(lib,"Winmm.lib")

#else
    #include <alsa/asoundlib.h>   

#endif

#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#include "file_info.h"

这里会对运行的系统环境进行判断然后选择合适的头文件。其中file_info.honWindows.h后续会提到,这里暂且按下不表。我们讲讲最开始运行的函数——主函数main

1.1 main

主函数main的功能为:

  • 创建线程函数
  • 提供选择不同功能的选项

main的内容为:

int main() 
{
    HANDLE pthread;					//线程的句柄
    int num = 0;                        
	if ((pthread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Listening, 0, 0, NULL)) == 0)
		printf("线程建立失败\n");

	while (1)
	{
		num = 0;
		printf("\n1、将音乐转成16位并输出为文件\n2、播放音乐\nQ/ESC 退出\n");
		printf("\n选择操作: ");

		kb = 0;						//重置并监听输入
		while (kb < 48 || kb >50)
		{
			if (back_flag == 1)break;
			Sleep(100);
		}

		if (kb == '1')
		{
			system("CLS");
			printf("\n将音乐转成16位并输出为文件\n\n");
			Output16bitFile(16);
		}

		else if (kb == '2')
		{
			system("CLS");
			printf("\n播放音乐\n");
			WaveOnWin();
		}

		else if (back_flag == 1)
		{
			printf("退出程序\n");
			break;
		}
	}

	if (pthread != 0)CloseHandle(pthread);
    return 0;
}

1.2 Listening

而线程函数Listening的内容就多了,除了监听键盘输入kb外,还有一些在非线程函数中不方便进行的判断,主要为

  1. 任何模式下
    • Q/ESC键时将返回标识符back_flag置1,表示返回到上个界面
    • X可以改变音乐播放顺序,即loop_mode
    • W/↑S/↓键时可以控制播放音量vol
  2. 音乐播放模式下(当device != NULL时)
    • 空格键时可以暂停/继续播放音乐
    • 暂停播放时按下任意控制音乐的按键解除暂停,例如Q,A,Z

线程函数Listening的代码为:

int back_flag = 0;				//返回标识符,当为1时程序从当前界面返回
int loop_mode;					//播放时的循环方向
int music_switch;				//判断切歌方向,1切下一首,-1切上一首
HWAVEOUT device;				//设备的句柄
char kb;                        //键盘输入

//用来监听键盘输入的线程,若有符合要求的按键输入
DWORD  WINAPI Listening(lpParam)
{
	int music_pause = 0;		//暂停标识符
	unsigned int vol = 0x7FFF7FFF;
	waveOutGetVolume(device, &vol);

	while (1)
	{
		kb = _getch();          //非阻塞无回显获取键盘输入

        //如果对音乐进行操作,例如切歌,判断是否处于暂停中,若是,则取消暂停
		if (kb == 'd' || kb == 'D' || kb == 77 ||			
			kb == 'a' || kb == 'A' || kb == 75 ||
			kb == 'q' || kb == 'Q' || kb == 27 ||
			kb == 'z' || kb == 'Z' ||
			kb == 'c' || kb == 'C' ||
			(kb > 48 && kb < 53))
		{
			if(music_pause = 1 && device != NULL)
			{
				waveOutRestart(device);
				music_pause = 0;
			}
		}
			
		else if (kb == 'w' || kb == 'W' || kb == 72)		// '↑',增加音量
		{
			if (vol + 0x19991999 < vol)
			{
				printf("音量已达最大值 ");
				vol = 0xFFFFFFFF;
			}
			else vol += 0x19991999;
			printf("音量 : %d\n", (vol >> 16) * 100 / 0xFFFF );
			waveOutSetVolume(device, vol);
		}

		else if (kb == 's' || kb == 'S' || kb == 80)		// '↓',降低音量
		{
			if (vol - 0x19991999 > vol)
			{
				printf("已静音 ");
				vol = 0x00000000;
			}	
			else vol -= 0x19991999;
			printf("音量 : %d\n", (vol >> 16) * 100 / 0xFFFF);
			waveOutSetVolume(device, vol);
		}

		else if (kb == ' ' && device != NULL)				// ' '暂停/播放音乐
		{
			if (music_pause == 0)
			{
				waveOutPause(device);
				printf("PAUSE\n");
				music_pause = 1; 
			}
			else
			{
				waveOutRestart(device);
				printf("RESTART\n");
				music_pause = 0;
			}
		}

		else if (kb == 'x' || kb == 'X')					//设置循环模式
		{
			if (device != NULL)
			{
				printf("当前循环模式为:");
				if (loop_mode == -1)
				{
					printf("单曲循环\n");
					music_switch = 0;
					loop_mode = 0;
				}
				else if (loop_mode == 0)
				{
					printf("顺序循环\n");
					music_switch = 1;
					loop_mode = 1;
				}
				else
				{
					printf("倒序循环\n");
					music_switch = -1;
					loop_mode = -1;
				}
			}
		}

        //退出程序,虽然目前只用来退出播放音乐和输出文件,但相信未来肯定大有用处
		if (kb == 'q' || kb == 'Q' || kb == 27)				
		{
			back_flag = 1;
		}
	}
}

代码中有个变量music_swtich,你可能会想不是有个loop_mode来控制切歌方向了吗?没错,我也是那么想的,但是这样按左右键切歌就会无比麻烦,所以只要把music_swtich当做是由左右键切歌和播放顺序loop_mode共同控制就好了。

2 file_info.c

好,介绍完了WaveOut.c,现在来讲file_info.c,不过在此之前,要先讲讲对应头文件file_info.h中定义的一个结构体,该结构体作用是存放.wav格式文件的文件头信息,这个wav结构体后面会频繁用到,代码为:

struct
{
	unsigned char identification[5];				//头文件文档标识
	unsigned int file_size;							//该数据+8为文件大小
	unsigned char format[5];						//文件类型
	unsigned char fmt[5];							//格式块标识
	unsigned int fmt_size;							//格式块长度
	unsigned short ecodeing_format;					//编码格式
	unsigned short channel;							//声道数量
	unsigned int Sampling_frequency;				//采样频率
	unsigned int transfer_speed;					//数据传输速度
	unsigned short Sample_size;						//采样帧大小
	unsigned short bit_depth;						//采样位深
	unsigned int music_size;						//音乐大小
}wav;

既然有了这个结构体存放文件头,那么肯定就需要函数来使用这个结构体了,接下来堂堂登场的是函数File_Info

2.1 File_Info(char filename[])

File_Info函数的作用就是读取.wav文件的文件头,然后存进上面提到的结构体当中,同时打印一部分较为有用的信息,代码为:

//显示文件信息
int File_Info(char filename[])
{
	FILE* fp = fopen(filename, "rb");       //只读打开文件

	if (fp == NULL)
	{
		printf("file open fail\n");
		return 0;
	}

	//读取文件头文件数据
	fread(wav.identification, 4,1, fp);
	fread(&wav.file_size, 4, 1, fp);
	fread(wav.format, 4,1, fp);
	fread(wav.fmt, 4, 1,fp);
	fread(&wav.fmt_size, 4, 1, fp);
	fread(&wav.ecodeing_format, 2, 1, fp);
	fread(&wav.channel, 2, 1, fp);
	fread(&wav.Sampling_frequency, 4, 1, fp);
	fread(&wav.transfer_speed, 4, 1, fp);
	fread(&wav.Sample_size, 2, 1, fp);
	fread(&wav.bit_depth, 2, 1, fp);
	fseek(fp, 40, SEEK_SET);
	fread(&wav.music_size, 4, 1, fp);

	//输出文件信息
	printf("\n音乐名称 : %s\n", filename);
	printf("文件大小 = %.3f Mb\n", wav.file_size / 1024.0 / 1024.0);
	printf("歌曲长度 = %dm%ds\n", (wav.music_size / wav.Sampling_frequency / wav.Sample_size) / 60,
		(wav.music_size / wav.Sampling_frequency / wav.Sample_size) % 60);
	printf("声道数量 = %d\n", wav.channel);
	printf("采样频率 = %d\n", wav.Sampling_frequency);
	printf("位深大小 = %d bit\n", wav.bit_depth);
	printf("采样帧   = %d\n", wav.Sample_size);
	printf("传输速率 = %d\n", wav.transfer_speed);
	/*fseek(fp, 0, SEEK_END);
	wav.music_size = ftell(fp) - 44;
	printf("实际歌曲长度 = %dm%ds\n", (wav.music_size / wav.Sampling_frequency / wav.Sample_size) / 60,
		(wav.music_size / wav.Sampling_frequency / wav.Sample_size) % 60);*/

	//关闭文件指针
	if (fclose(fp) == EOF)
	{
		puts("Fail to close");
		exit(0);
	}
	return 0;							
}

细心的人已经注意到有一段注释,内容是打印出文件的实际长度,这个是为了和文件头中的文件/数据大小信息进行一个相互印证,一般来讲是没必要的,但是我在实际读取过程中发现许多.wav文件头中这部分信息是错误的,所以留了这么一串代码在这。

2.2 Bit_Conversion(unsigned long buffsize,char* data,char* data_out,int bit,int file_bit)

Bit_Conversion函数的功能是转换位深。它可以接收两块内存,一块为读取到的文件信息,一块则为空,它通过获取的目标位深(16bit)和文件读取的位深,以及传递过来的文件信息内存长度来计算,将计算后的数据填入空内存中,然后返回空内存(虽然经过填充已经不空了)的头指针。

具体计算过程为

  • 8bit->16bit:此时空内存长度为文件信息内存长度的两倍,将文件信息中的每一字节按隔一个字节的形式放入空内存中,因为8位文件中的信息是无符号的,而16位文件是有符号的,所以还需要再将复制来的字节高位取反
  • 16bit->16bit:这个大伙都知道,就不赘述了
  • 24bit->16bit:将文件中三个字节看作是一个单位,取后面两个放入空内存中
  • 32bit->16bit:将文件中每两个字节,取后面一个放入空内存中
  • 额外注意的是,.wav格式文件字节都是小端方式存储,比如16位中的某一个数据7E 11,这里其实11是高8位,而7E是低8位,所以在上述的数据操作中,总是要取一个单位(16位中就是两个字节)中后面的字节

那么来看看代码是怎么写的吧

//位深转换
char* Bit_Conversion(unsigned long buffsize, char* data, char* data_out, int bit, int file_bit)
{

	if (file_bit == 8)												
	{
		for (unsigned int i = 0; i < buffsize/2; i += 1)					
		{
			data_out[i * 2] = 0x00;									
			data_out[i * 2 + 1] = data[i] ^ 0x7F;				
		}
	}
	else if (file_bit == 24)							//每三个字节,只取后两位,即高位的那两位
	{
		for (unsigned int i = 0; i < buffsize; i += 2)					
		{
			data_out[i] = data[i / 2 * 3 + 1];
			data_out[i + 1] = data[i / 2 * 3 + 2];
		}
	}
	else if (file_bit == 32)							//缩小数据,每两位只取高位
	{
		for (unsigned int i = 0; i < buffsize; i += 1)					
		{
			data_out[i] = data[i * 2 + 1];
		}
	}
	else
	{
		for (unsigned int i = 0; i < buffsize; i += 1)
		{
			data_out[i] = data[i];
		}
	}

	return (char*)data_out;
}

写的真简洁明了是不是?好,接下来介绍的是单独的一个功能模块,在程序开始时按1就可以进入这个函数了

2.3 Output16bitFile(int bit)

Output16bitFile(int bit)函数的功能是将文件转换为16位文件并输出为new_file.wav文件,这个逻辑其实挺简单的,但是实现却比较繁琐,毕竟转换完还要对文件头进行疯狂修改,实现逻辑为:

  1. 读取键盘输入:若没有输入则等待,有数字输入进入下一步,有back_flag1则退出
  2. 显示原文件信息:调用File_Info显示信息,顺便填充wav结构体
  3. 将原文件文件头写入新文件:创建新文件,同时写入读到的文件头信息
  4. 读取原文件数据进行转换:调用Bit_Conversion函数完成
  5. 将转换好的数据写入新文件
  6. 持续步骤4步骤5,直到读完:事前获得文件尾位置,当读到文件尾时修改读取大小,争取把文件全部读完
  7. 读完后修改新文件文件头:主要改文件/数据大小,读文件尾位置完成,而非计算得到,防止原文件文件头是错误的
  8. 显示新文件信息
  9. 完成一次循环,返回到步骤1:这里关闭文件指针,释放读取文件信息的内存,但不释放空内存,因为文件位深不一样,但是转换后的位深是一样的,只有退出时才清理空内存

看吧,说了想着简单,结果写着挺麻烦的,特别是还得按键控制,那就更繁琐了,不说了直接看代码:

char kb;
int bcak_flag;
int Output16bitFile(int bit)							//将文件转换为16bit并以文件的形式输出
{
	unsigned char header[44];							//存放头文件的数组
	char* data_in = NULL;										
	char* data_out = NULL;
	int output_size = 10000;							//一次转换的数据大小
	int data_size = 0;									//文件大小
	int cycle_flag = 1;									//循环标识符
	char file_name[100];
	bit = 16;

	data_out = (char*)malloc(output_size);				//写入新文件的数据
	if (data_out == NULL)return 0;

	printf("1. 红色高跟鞋_8bit.wav\n");
	printf("2. 红色高跟鞋_16bit.wav\n");
	printf("3. 最伟大的作品_24bit_48000hz.wav\n");
	printf("4. Dark side of the moon_192K_32bit.wav\n");
	printf("Q/ESC返回\n");

	while (1)
	{
		cycle_flag = 1;									//为下次循环做准备
		output_size = 10000;
		memset(file_name, 0, sizeof(file_name));
		printf("\n选择音乐:\n");

		kb = 0;
		while (kb < 49 || kb>52)
		{
			if (back_flag == 1)
				break;
			Sleep(100);									//不加的话一直点'↑'键会跳到下面的程序
		}

		system("CLS");
		printf("\n将音乐转成16位并输出为文件\n\n");
		printf("1. 红色高跟鞋_8bit.wav\n");
		printf("2. 红色高跟鞋_16bit.wav\n");
		printf("3. 最伟大的作品_24bit_48000hz.wav\n");
		printf("4. Dark side of the moon_192K_32bit.wav\n");
		printf("Q/ESC返回\n");

		if (kb == 49)strcpy(file_name, "red_8bit.wav");			
		else if (kb == 50)strcpy(file_name, "red.wav");					
		else if (kb == 51)strcpy(file_name, "44K_24bit.wav");		
		else if (kb == 52)strcpy(file_name, "192K_32bit.wav");		
		else if (back_flag == 1)
		{
			back_flag = 0;
			printf("退出音乐输出\n");
			system("CLS");
			break;
		}

		printf("\n音乐:%s 转换16bit输出完成\n", file_name);
		printf("\n转换前音乐数据");
		File_Info(file_name);									//获取文件相关信息,大小,位深等
		FILE* file_read = fopen(file_name, "rb");				//只读打开文件
		//char new_file = strcat("new_",filename);
		FILE* file_write = fopen("new_file.wav", "wb");			//创建新文件

		if (file_read == NULL)
		{
			printf("file read fail\n");
			return 0;
		}

		if (file_write == NULL)
		{
			printf("file write fail\n");
			return 0;
		}
								
		fseek(file_read, 0, SEEK_END);
		data_size = ftell(file_read);							//文件尾大小

		fseek(file_read, 0, SEEK_SET);		//先复制头文件到新文件中,等转换完再修改里面各项内容
		fread(header, sizeof(char), 44, file_read);
		fseek(file_write, 0, SEEK_SET);
		fwrite(header, sizeof(char), 44, file_write);
		fseek(file_write, 44, SEEK_SET);

		data_in = (char*)malloc(output_size * wav.bit_depth / bit);	//读取原本文件的数据
		if (data_in == NULL)return 0;

		while (cycle_flag)
		{
            //当读到文件尾时,剩余数据不足一次读取,故对读取大小进行修改
			if (ftell(file_read) + output_size >= data_size)		
			{
				output_size = data_size - ftell(file_read);
				cycle_flag = 0;
			}

            //从原本文件中读取文件,然后用转换函数转换完后写入新文件
			fread(data_in, sizeof(char), output_size * wav.bit_depth / bit, file_read);
			Bit_Conversion(output_size, data_in, data_out, bit, wav.bit_depth);				
			fwrite(data_out, sizeof(char), output_size, file_write);
		}

		fseek(file_write, 0, SEEK_END);					//计算新的文件大小、数据传输速度、采样帧大小
		data_size = ftell(file_write);
		wav.transfer_speed = wav.transfer_speed * bit / wav.bit_depth;
		wav.Sample_size = wav.Sample_size * bit / wav.bit_depth;
		wav.bit_depth = bit;

		fseek(file_write, 4, SEEK_SET);							//写入新的头文件数据
		data_size -= 8;
		fwrite(&data_size, 4, 1, file_write);					//文件数据长度
		fseek(file_write, 28, SEEK_SET);
		fwrite(&wav.transfer_speed, 4, 1, file_write);			//数据传输速率
		fwrite(&wav.Sample_size, 2, 1, file_write);				//采样帧大小
		fwrite(&wav.bit_depth, 2, 1, file_write);				//位深
		fseek(file_write, 40, SEEK_SET);
		data_size = data_size + 8 - 44;
		fwrite(&data_size, 4, 1, file_write);					//数据块长度
		
		free(data_in);
		fclose(file_read);
		fclose(file_write);
		printf("\n转换后音乐数据");
		File_Info("new_file.wav");
	}

	free(data_out);
	return 0;
}

3. onWindows.c

讲完了File_Inof.c这个开胃菜后,现在来到了重量级onWindows.c,这个我都不知道从何讲起,甚至名字都不知道为什么要这么取的(其实是因为那时候要搞中间层,就把这个当作是Windows下的播放代码了)。算了,还是老规矩,从主函数main2选项之后讲起吧。

3.1 WaveOnWin()

当你在主界面选择想听点歌时,程序就进入了WaveOnWin()当中。相比于待会要介绍的writeAudioBlock,这个函数可以说是比较简单了。它的运行逻辑为:

  1. 初始化设置:比如建立WAVEFORMATEX,初始化临界区变量,初始化数组等
  2. 监听键盘输入kb,如果有数字输入就放对应音乐,没有就等待,如果有退出输入就退出
  3. 进入循环,监听键盘输入kb,并显示音乐信息:音乐之间的循环,这里其实也有监听,但是由于kb没有清零,所以这里第一次进入循环,和后续播放过程中按了数字键后能直接播放对应音乐,在使用完kb后才会清零,调用File_Info显示信息,顺便填充wav结构体
  4. 还有music_size,一般是1,也就是顺序播放,但是如果是左右按键切歌则置1-1,在赋值给num(音乐序号)后才会等于loop_mode,保证后续的循环正常
  5. wav结构体内信息赋给WAVEFORMATEX结构体成员
  6. 打开播放设备并进入writeAudioBlock中:这里就留着下一部分再讲吧
  7. 等待程序从writeAudioBlock退出,从writeAudioBlock退出后关闭设备,清理device设备句柄。如果不是因为back_flag退出则再次进入循环步骤3,否则则退出
  8. 退出时back_flag0

代码为:

int WaveOnWin(void)
{
	WAVEFORMATEX wave;								//初始化wave设置
	InitializeCriticalSection (&KEY);				//初始化临界区关键字
	int bit = 16;
	int num = 0;
	char file_name[100];
	memset(file_name, 0, sizeof(file_name));
	music_switch = 1;								//默认切歌方向

	printf("\n1. 红色高跟鞋_8bit.wav\n");
	printf("2. 红色高跟鞋_16bit.wav\n");
	printf("3. 最伟大的作品_24bit_48000hz.wav\n");
	printf("4. Dark side of the moon_192K_32bit.wav\n");
	printf("Q/ESC返回主界面\n");
	printf("\n选择音乐:\n");

	kb = 0;											//重置并监听输入
	while (1)
	{
		if (kb > 48 && kb < 53)break;
		else if (back_flag == 1)
		{
			back_flag = 0;
			system("CLS");
			return 0;
		}
	}
	
	while (!back_flag)
	{
		system("CLS");
		printf("\n播放音乐\n");
		printf("\n1. 红色高跟鞋_8bit.wav\n");
		printf("2. 红色高跟鞋_16bit.wav\n");
		printf("3. 最伟大的作品_24bit_48000hz.wav\n");
		printf("4. Dark side of the moon_192K_32bit.wav\n");
		printf("Q/ESC返回主界面  W/↑增加音量  S/↓降低音量  A/←上一首  D/→下一首  Z/C快进/倒退5s  空格暂停/播放\n");

		if (kb > 48 && kb < 53)						//如果是通过数字按键切歌,则不参与loop_mode
		{
			num = (int)kb - 48 - 1;
			kb = 0;
		}
		else										//否则按照循环模式或A/D按键切换音乐
			num = (num + music_switch + 4) % 4;							
		if (num == 0)strcpy(file_name, "red_8bit.wav");					
		else if (num == 1)strcpy(file_name, "red.wav");						
		else if (num == 2)strcpy(file_name, "44K_24bit.wav");		
		else if (num == 3)strcpy(file_name, "192K_32bit.wav");			

		//由于music_switch由切歌按键和循环模式共同影响,在此重置则不管之前进行了什么操作,下一首播放的音乐依旧可以遵循循环模式
		music_switch = loop_mode;											
		File_Info(file_name);

		//填写WAVEFORMATEX
		wave.nSamplesPerSec = wav.Sampling_frequency;						//采样频率
		wave.wBitsPerSample = bit;											//采样位深
		wave.nChannels = wav.channel;										//音道
		wave.cbSize = 0;													//附加信息
		wave.wFormatTag = WAVE_FORMAT_PCM;									//PCM编码格式,赋1
		wave.nBlockAlign = wave.wBitsPerSample * wave.nChannels / 8;		//帧大小
		wave.nAvgBytesPerSec = wave.nBlockAlign * wave.nSamplesPerSec;		//传输速率

		//输出文件信息
		printf("\n以16bit时播放信息为: \n");
		printf("采样频率 = %d\n", wave.nSamplesPerSec);
		printf("位深 = %d bit\n", wave.wBitsPerSample);
		printf("采样帧大小 = %d\n", wave.nBlockAlign);
		printf("数据传输速率 = %d\n", wave.nAvgBytesPerSec);
		if (loop_mode == 1)printf("当前循环模式为(按‘X’切换):顺序循环\n");
		else if (loop_mode == 0)printf("当前循环模式为(按‘X’切换):单曲循环\n");
		else printf("当前循环模式为(按‘X’切换):倒序循环\n");

		//WAVE_MAPPER 是 mmsystem.h 中定义的常量,它始终指向系统上的默认波形设备
		if (waveOutOpen(&device, WAVE_MAPPER, &wave, (DWORD_PTR)waveOutProc, 0, CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
		{
			fprintf(stderr, "unable to open WAVE_MAPPER device\n");
			return 0;
		}

		writeAudioBlock(device, file_name, file_lrc, bit);
		waveOutClose(device);								//播放完音频后关闭设备,清理句柄
		printf("Close device successful\n");
		device = NULL;
	}

	system("CLS");
	back_flag = 0;
	return 0;						
}

细心的人会发现调用waveOutOpen时启动了回调函数,没错,接下来是waveOutProc

3.2 CALLBACK waveOutProc(HWAVEOUT device, UINT uMsg, DWORD dwInstance, WAVEHDR dwParam1, DWORD dwParam2)

这个没什么好说的,就是当有缓冲区输出完时,设置函数中的uMsgWOM_DONE,所以就用这个判断是否有缓冲区输出完,有的话就free_buff+1,free_buff是实时的可用缓冲区数量,并不等于SUM_BUFF。你是不是想为什么是+1不是-1,因为-1是有缓冲区开始进行输出,而这里是缓冲区输出完了,可以使用它了,所以+1。这里用临界区防止与writeAudioBlock共享资源时冲突。代码为:

//回调函数,检测输出完的缓冲区,当有缓冲区输出完会调用该函数
static void CALLBACK waveOutProc(HWAVEOUT device, UINT uMsg, DWORD dwInstance, WAVEHDR dwParam1, DWORD dwParam2)
{
	if (uMsg != WOM_DONE)
	{
		return;
	}

	EnterCriticalSection(&KEY);			//使用临界区防止与主函数冲突,同时可用缓冲区加一
		free_buff++;
	LeaveCriticalSection(&KEY);
}

不多说了,直接进入writeAudioBlock,要准备下班了,而且是2月3号的下班。

3.3 writeAudioBlock(HWAVEOUT device,char filename[], char file_lrc[], int bit)

我的确把过多的功能和判断集成在了这个函数中,导致这个函数过于臃肿。其实这个函数的功能就是播放音乐,然后在播放的同时搞点判断,没了,判断的话具体参考:

Q/ESC 返回主界面
W/↑ 增加音量 S/↓降低音量
A/← 上一首 D/→ 下一首
Z/C 快进/倒退5s
空格 暂停/播放
数字 直接切到该序号的音乐

writeAudioBlock的运行逻辑为:

  1. 初始化,初始化各种东西:定义了一个WAVEHDR结构体数组wave_buff[SUM_BUFF],和一个指针数组out_data[SUM_BUFF]SUM_BUFF为自己定义的缓冲区数量。指针数组是为了待会申请内存,理论上来讲两块内存就够了,但是那样有点麻烦,就使用指针数组了,不过也问题不大,只需要把SUM_BUFF设置为2,那么就是两块内存了
  2. 打开音乐文件,获得文件大小,申请file_data内存存放文件信息,读取BUFFER_LENGTH * file_bit / bit大小的信息,BUFFER_LENGTH为一次转换的大小,BUFFER_LENGTH * file_bit / bit的意义是控制读取时的信息大小,比如32位转16位播放,那么要读2倍的BUFFER_LENGTH才不会出问题,而输出只需要正常的BUFFER_LENGTH就行
  3. 进入循环,调用Bit_Conversion后获得转换后数据的内存的指针,然后调用waveOutPrepareHeader清理缓冲区(也就是刚刚获得的内存),再调用waveOutWrite输出音频数据。因为输出了一块缓冲区,所以让free_buff减一很合理吧,这里用临界区防止与线程共享资源时冲突。
  4. 检测是否有上一块缓冲区,有的话进行清理,当从第一块缓冲区开始时,上一块缓冲区指针为NULL
  5. 等待free_buff+1,也就是缓冲区输出完,毕竟播放音乐是需要时间的
  6. 进入判断阶段,因为一次循环极快,所以当有键盘输入时也可以反应过来,有哪些判断就看上面功能介绍就行了
  7. 一通判断后,如果是:
    • 快进/倒退,则改变文件指针位置
    • 切歌,则退出循环,将music_size置1或-1
    • 有数字按键也退出循环,保留kbWaveOnWin中,这样就跟第一次进入播放模式时的逻辑一样了
    • 读完文件退出循环,music_sizeloop_mode
    • 有退出标识符则退出循环
  8. 如果有幸没有退出循环,则回到步骤3,持续读取、输出文件内容到设备中
  9. 结束循环后,释放这个函数里使用的内存,毕竟下一次又是全新的音乐了

代码如下:

//向设备写入缓冲区数据
void writeAudioBlock(HWAVEOUT device,char filename[], char file_lrc[], int bit)
{
	WAVEHDR wave_buff[SUM_BUFF];						//设置缓冲区结构体数组
	int file_bit = wav.bit_depth;						//文件位深,默认16位
	FILE* file = NULL;						
	char* file_data = NULL;								//读取的文件数据	
	char* out_data[SUM_BUFF];							//位深转换后要输出的数据

	file = fopen(filename, "rb+");
	if (file == NULL)
	{
		printf("无法打开文件\n");
		return;
	}
			
	//初始化缓存区		
	for (int i = 0;i < SUM_BUFF;i++)
	{
		ZeroMemory(&wave_buff[i], sizeof(WAVEHDR));
		wave_buff[i].dwBufferLength = BUFFER_LENGTH;								
		wave_buff[i].lpData = NULL;
		wave_buff[i].dwFlags = 0;
		wave_buff[i].dwLoops = 1;
		out_data[i] = (char*)malloc(BUFFER_LENGTH);				//申请data缓存区读取文件
		if (out_data[i] == 0)return;
	}

	fseek(file, 0, SEEK_END);								    //读取文件结尾位置,用来判断文件是否结束
	long file_tile = ftell(file);

	file_data = (char*)malloc(BUFFER_LENGTH * file_bit / bit);	//申请data缓存区读取文件
	if (file_data == 0)return;										
	fseek(file, 44, SEEK_SET);			
							
    //BUFFER_LENGTH * file_bit/bit指文件数据要读取的大小,如16bit文件转8bit输出
    //假设要输出44100字节数据,则要读取文件44100*2字节数据,将其中一半数据去除再输出。
	fread(file_data, sizeof(char), BUFFER_LENGTH * file_bit / bit, file);

	int now = 0;		//标识当前在播放的缓冲区
	int next = 0;		//标识下一个缓冲区
	int last = 0;		//标识上一个缓冲区
	kb = 0;				//重置输入

	while (1)
	{
		next = (now + 1) % SUM_BUFF;				//直接 +1 固然美好,但是加个 % 就可以循环了
		last = (now + SUM_BUFF - 1) % SUM_BUFF;		//直接 -1 会出现负数

		wave_buff[now].lpData = Bit_Conversion(BUFFER_LENGTH, file_data, out_data[now], bit, file_bit);
		waveOutPrepareHeader(device, &wave_buff[now], sizeof(WAVEHDR));
		waveOutWrite(device, &wave_buff[now], sizeof(WAVEHDR));  

		EnterCriticalSection(&KEY);			 //可用缓冲区数量减一,同时使用临界区防止与回调函数冲突	
		free_buff--;
		LeaveCriticalSection(&KEY);

		if (wave_buff[last].lpData != NULL)	//当前的缓冲区在输出时,偷偷释放上一块缓冲区
		{
			waveOutUnprepareHeader(device, &wave_buff[last], sizeof(WAVEHDR));
		}

		//当未输出完\还没有缓冲区时等待,一般来讲,输出时间远大于运行时间,除非每次输出长度小的可怜
		while (free_buff <= 0) 													
		{																		
			Sleep(10);															
		}

		if (kb == 'c' || kb == 'C')				//快进5秒
		{																		
            //注释内容为:如果到结尾继续快进,则返回到结尾前5s,而非直接跳到下一个文件,注释后是快进到文件结尾时切进下一首歌
			/*if (ftell(file) >= (file_tile - BUFFER_LENGTH * file_bit / bit * 20))	
				fseek(file, -(BUFFER_LENGTH * file_bit / bit * 20), SEEK_END);
			else*/
			fseek(file, BUFFER_LENGTH * file_bit / 8 * 10, SEEK_CUR);
			printf("快进5秒\n");
			kb = 0;
		}

		else if (kb == 'z' || kb == 'Z')	    //倒退5秒
		{
			if (ftell(file) < BUFFER_LENGTH * file_bit / 8 * 10)	//在文件开头倒退时重置到文件头
				fseek(file, 44, SEEK_SET);
			else
				fseek(file, -(BUFFER_LENGTH * file_bit / 8 * 10), SEEK_CUR);
			printf("倒退5秒\n");
			kb = 0;
		}

        //读取下一次循环要用的数据
		fread(file_data, sizeof(char), BUFFER_LENGTH * file_bit / bit, file);	
        //读完文件、结束标识符为1或输入数字,结束循环
		if (ftell(file) >= file_tile || back_flag == 1 || (kb > 48 && kb < 53))	
		{
			while (waveOutUnprepareHeader(device, &wave_buff[now], sizeof(WAVEHDR)) == WAVERR_STILLPLAYING)
				Sleep(10);
			printf("\nMusic End?\n");
			break;
		}
	    //如果输入了‘d’或者→按键,切歌标识符置1,结束循环
		else if (kb == 'd' || kb == 'D' || kb == 77)	
		{
			music_switch = 1;
			while (waveOutUnprepareHeader(device, &wave_buff[now], sizeof(WAVEHDR)) == WAVERR_STILLPLAYING)
				Sleep(10);
			printf("\nThe Next Song\n");
			break;
		}
        //如果输入了‘a’或者←按键,切歌标识符置-1,结束循环
		else if (kb == 'a' || kb == 'A' || kb == 75)   
		{
			music_switch = -1;
			while (waveOutUnprepareHeader(device, &wave_buff[now], sizeof(WAVEHDR)) == WAVERR_STILLPLAYING)
				Sleep(10);
			printf("\nThe Last Song\n");
			break;
		}
		now = next;
	}

	//结束播放后释放内存
	Sleep(500);
	for (int i = 0;i < SUM_BUFF;i++)
	{
		free(out_data[i]);								
	}
	free(file_data);
	fclose(file);
}

不得不说,这一块写的的确有点过于繁杂臃肿了,本想将判断全部移入线程,但是线程本身必须要判断的就已经够多了,且还得用全局变量区传递信息,就又想将线程中的判断全部判断移到这里面来,但是有些判断完全不知道怎么移(比如暂停),就只好将大部分判断放在这,必要的就留在线程里了。感觉还是可以改进改进的,不过功能加的是真的爽

4. 总结

还需要总结吗?应该不用了吧。那就讲讲整个项目的运行过程吧

  1. 运行程序
  2. 创建并启动线程,开启全程监听键盘输入
  3. 主界面,选择功能,等待输入
  4. 如果选1则进入文件输出模式,可以疯狂转换文件,按Q/ESC退回到主界面
  5. 如果选2则进入音乐播放模式,可以疯狂地听音乐,按Q/ESC退回到主界面
  6. 回到主界面,即回到步骤3
  7. 主界面按Q/ESC退出程序

好了,上面就是整个运行过程了,感觉内容不多,但是为什么我写了那么多代码,真是奇怪啊

  • 21
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值