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

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

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

先展示主函数界面:

int main(){WAVEFORMATEX wave;//初始化wave设置HWAVEOUT device;//设备句柄,设置为全局变量是为了送给主函数的线程InitializeCriticalSection(&KEY);//初始化临界区关键字//填写WAVEFORMATEXwave.nSamplesPerSec = 48000;//采样频率wave.wBitsPerSample = 24;//采样位深wave.nChannels = 2;//音道wave.cbSize = 0;//附加信息wave.wFormatTag = WAVE_FORMAT_PCM;//PCM编码格式,也可以赋1wave.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_LENGTH44100 * 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 = 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 #include #include #include #pragma comment(lib,"Winmm.lib")#define SUM_BUFF 3//缓冲区的数量,必须大于等于2#define BUFFER_LENGTH44100 * 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 = 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);//初始化临界区关键字//填写WAVEFORMATEXwave.nSamplesPerSec = 48000;//采样频率wave.wBitsPerSample = 24;//采样位深wave.nChannels = 2;//音道wave.cbSize = 0;//附加信息wave.wFormatTag = WAVE_FORMAT_PCM;//PCM编码格式,也可以赋1wave.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接口的全部工作了,赶快来运行下试试看吧。总感觉这次记录好像车轱辘话有点多,叹,下次再改吧。