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

好的各位亲爱的观众朋友们,现在开始我们的学习笔记-C语言关于waveout接口的使用(三)。

在上一篇中提到,想不使用那么大的内存,就要循环读取音频文件数据,但是如果只是单纯的循环的话,每个缓冲区之间又有很明显的卡顿,那么我们该怎么办呢?

我们需要多个缓冲区来替换以前的单个缓冲区,这样当一个缓冲区播放完,下一个缓冲区可以马上接着播放,同时再清理播放完的缓冲区以及准备下下个缓冲区,这样当下个缓冲区播放完的时候,又可以有一个准备好的缓冲区接替上,这样我们就消除了由于需要一直重复准备/输出/清理单个缓冲区所带来的卡顿。

先展示主函数界面:

int main()
{
	WAVEFORMATEX wave;								//初始化wave设置
	HWAVEOUT device;								//设备句柄,设置为全局变量是为了送给主函数的线程
	InitializeCriticalSection(&KEY);				//初始化临界区关键字

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

	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;
	}

	WriteBuff(device, "最伟大的作品.wav");
	waveOutClose(device);							//播放完音频后关闭设备,清理句柄
	printf("Close device successful\n");
	return 0;
}

各位会发现除了原先有的内容外多了点新的内容,也少了点内容。其中最明显的是,waveOutPrepareHeader;waveOutWrite;waveOutUnprepareHeader;这三个函数被移入了WriteBuff当中,因为如果把它们几个写在主函数中就导致主函数实在太臃肿了,所以就挪到调用函数WriteBuff中。

WriteBuff函数中,我们首先需要WAVEHDR结构体数组并且设置好,至于申请内存理论上来讲两块就够了,但是那样用起来就比较麻烦了,所以就随着数组来,多个个数组就多少块内存吧,毕竟44k(可以自己设置)的内存申请多少块应该都是没问题的吧。写起来也很简单,就不赘述了。如下所示:

	#define SUM_BUFF 3						//缓冲区的数量,必须大于等于2
    #define BUFFER_LENGTH  44100 * 1		//单个缓冲区保存的数据长度,也是每次读取的数据长度

    WAVEHDR wave_buff[SUM_BUFF] = {NULL};				//设置缓冲区结构体数组
	char* file_data[SUM_BUFF] = {NULL};					//读取的文件数据	

	//初始化缓存区		
	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;
		file_data[i] = (char*)malloc(BUFFER_LENGTH);	//申请data缓存区读取文件
		if (file_data[i] == 0)return;
	}

现在有了WAVEHDR结构体数组和对于的内存后,我们还需要知道一个缓冲区什么时候播放完,这样才能让下一块缓冲区接上。

或许你会说,waveOutWrite完后不就可以让下一块上了吗,错误的,waveOutWrite之间是阻塞式的,哪怕你waveOutWrite许多次,也是一个一个播放的,但是waveOutWrite和接下里的要运行的函数却是非阻塞式的,就是说一运行完waveOutWrite就马上运行下面的内容了,大伙可以翻回去看waveOutUnprepareHeader那里就是等待输出完,如果不等待的话就会清理失败。

说实话写到这我突然意识到可不可以用waveOutUnprepareHeader的返回值来判断是否输出完呢?不过我都写了别的方法了,只好硬着头皮继续写下去了。

这里我们使用waveOut的专用回调函数waveOutProc来判断,只需要这么设置就可以了:waveOutOpen(&device, WAVE_MAPPER, &wave, (DWORD_PTR)waveOutProc, 0, CALLBACK_FUNCTION)

相信仔细阅读过waveOutOpen 函数 (mmeapi.h) - Win32 apps | Microsoft Learn的人都知道,设置CALLBACK_FUNCTION是向回调函数的指针,同时当一个缓冲区播放完时,回调函数中的参数uMsg 都会等于 WOM_DONE。所以,我们设置一个变量,表示当前可用的缓冲区数量,当主函数中有缓冲区输出时它-1,当回调函数中uMsg == WOM_DONE时+1。但是回调函数是线程,主函数和线程函数同时对一个变量进行操作很容易出错,那么我们就需要使用临界区来保护变量,防止两个函数同时对它进行操作。

临界区用起来很简单,总共就需要

        static CRITICAL_SECTION CRITION;           //新建临界区变量

        InitializeCriticalSection(&CRITION);             //初始化临界区

        EnterCriticalSection(&CRITION);                 //进去临界区

        LeaveCriticalSection(&CRITION);                //离开临界区

其中CRITION表示临界区变量,这个怎么设置都行,其实就相当于一个标识符,用来和其他不相干但是也需要临界区保护的变量区别开。打个比方,就像玩游戏一样,一个账号已经有人玩了,你再想玩就得等人家玩完退出你才能进去,但是你也可以登别的账号(用别的临界区变量),访问别的账号里的内容,但前提是登录这个账号不会影响到前一个账号。

//回调函数,检测输出完的缓冲区,当有缓冲区输出完会调用该函数
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);
}

所以经过一点小小的完善细节,WriteBuff函数的代码如下:

//向设备写入缓冲区数据
void WriteBuff(HWAVEOUT device, char filename[])
{
	WAVEHDR wave_buff[SUM_BUFF] = {NULL};	//设置缓冲区结构体数组
	FILE* file = NULL;
	char* file_data[SUM_BUFF] = {NULL};		//读取的文件数据	

	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;
		file_data[i] = (char*)malloc(BUFFER_LENGTH);    //申请data缓存区读取文件
		if (file_data[i] == 0)return;
	}

	fseek(file, 0, SEEK_END);		//读取文件结尾位置,用来判断文件是否结束
	long file_tile = ftell(file);
	fseek(file, 44, SEEK_SET);	    //读取文件数据,为缓冲区[0]做准备									
	fread(file_data[0], sizeof(char), BUFFER_LENGTH, file);

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

	while (1)
	{
		next = (now + 1) % SUM_BUFF;	//直接 +1 固然美好,但是加个 % 就可以循环了
        //例如现在是3,总数是5,那么(3+5-1)% 5 = 2,实现往后倒一位,直接 -1 会出现负数
		last = (now + SUM_BUFF - 1) % SUM_BUFF;

		wave_buff[now].lpData = file_data[now];
		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);														

		fread(file_data[next], sizeof(char), BUFFER_LENGTH, file);	//读取下一次循环要用的数据
		if (ftell(file) >= file_tile)		//读完文件,结束循环
		{
			while (waveOutUnprepareHeader(device, &wave_buff[now], sizeof(WAVEHDR)) == WAVERR_STILLPLAYING)
				Sleep(10);
			printf("\nMusic End\n");
			break;
		}

		now = next;
	}

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

当然,还有完整代码,如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <mmsystem.h>
#pragma comment(lib,"Winmm.lib")

#define SUM_BUFF 3						//缓冲区的数量,必须大于等于2
#define BUFFER_LENGTH  44100 * 1		//单个缓冲区保存的数据长度,也是每次读取的数据长度
static CRITICAL_SECTION KEY;			//设置临界区,使回调函数和主函数不会冲突
static int free_buff = 2;				//可用的缓冲区数量	

//回调函数,检测输出完的缓冲区,当有缓冲区输出完会调用该函数
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);
}

//向设备写入缓冲区数据
void WriteBuff(HWAVEOUT device, char filename[])
{
	WAVEHDR wave_buff[SUM_BUFF] = {NULL};						//设置缓冲区结构体数组
	FILE* file = NULL;
	char* file_data[SUM_BUFF] = {NULL};							//读取的文件数据	

	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;
		file_data[i] = (char*)malloc(BUFFER_LENGTH);							//申请data缓存区读取文件
		if (file_data[i] == 0)return;
	}

	fseek(file, 0, SEEK_END);													//读取文件结尾位置,用来判断文件是否结束
	long file_tile = ftell(file);
	fseek(file, 44, SEEK_SET);													//读取文件数据,为缓冲区[0]做准备									
	fread(file_data[0], sizeof(char), BUFFER_LENGTH, file);

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

	while (1)
	{
		next = (now + 1) % SUM_BUFF;											//直接 +1 固然美好,但是加个 % 就可以循环了
		last = (now + SUM_BUFF - 1) % SUM_BUFF;									//例如现在是3,总数是5,那么(3+5-1)% 5 = 2,实现往后倒一位,直接 -1 会出现负数

		wave_buff[now].lpData = file_data[now];
		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);														

		fread(file_data[next], sizeof(char), BUFFER_LENGTH, file);				//读取下一次循环要用的数据
		if (ftell(file) >= file_tile)											//读完文件,结束循环
		{
			while (waveOutUnprepareHeader(device, &wave_buff[now], sizeof(WAVEHDR)) == WAVERR_STILLPLAYING)
				Sleep(10);
			printf("\nMusic End\n");
			break;
		}

		now = next;
	}

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

int main()
{
	WAVEFORMATEX wave;								//初始化wave设置
	HWAVEOUT device;								//设备句柄,设置为全局变量是为了送给主函数的线程
	InitializeCriticalSection(&KEY);				//初始化临界区关键字

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

	//尝试打开默认的 Wave 设备。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;
	}

	WriteBuff(device, "最伟大的作品.wav");
	waveOutClose(device);							//播放完音频后关闭设备,清理句柄
	printf("Close device successful\n");
	return 0;
}

好,至此就完成以小规模读取文件内容的waveOut接口的全部工作了,赶快来运行下试试看吧。总感觉这次记录好像车轱辘话有点多,叹,下次再改吧。

  • 38
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值