好的各位亲爱的观众朋友们,现在开始我们的学习笔记-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接口的全部工作了,赶快来运行下试试看吧。总感觉这次记录好像车轱辘话有点多,叹,下次再改吧。