在使用SDL进行音频解码的时候涉及到一个回调函数,这里有点复杂,初学不容易搞明白,做点记录。
1 SDL_AudioSpec结构体与SDL_OpenAudio()函数
简单地说,SDL_AudioSpec结构体中是与SDL进行音频解码相关的参数的一个结构体。文档中内容如下;
SDL_AudioSpec
Name
SDL_AudioSpec -- Audio Specification Structure
Structure Definition
typedef struct{
int freq;
Uint16 format;
Uint8 channels;
Uint8 silence;
Uint16 samples;
Uint32 size;
void (*callback)(void *userdata, Uint8 *stream, int len);
void *userdata;
} SDL_AudioSpec;
其中各成员的意义如下:
freq | Audio frequency in samples per second |
format | Audio data format |
channels | Number of channels: 1 mono, 2 stereo |
silence | Audio buffer silence value (calculated) |
samples | Audio buffer size in samples |
size | Audio buffer size in bytes (calculated) |
callback(..) | Callback function for filling the audio buffer |
userdata | Pointer the user data which is passed to the callback function |
#include "SDL.h"
int SDL_OpenAudio(SDL_AudioSpec *desired, SDL_AudioSpec *obtained);
在SDL_OpenAudio()的文档中,此函数用desired指定的参数来打开音频设备并成功时返回0,并将真正的硬件参数防盗obtained参数指定的结构体中。
其中的desired->callback,这个函数指针是SDL内部在当音频设备已经准备好处理接下来的数据的时候SDL进行回调的。其中传入的stream参数是指向SDL内部的音频缓冲区的指针,len参数指向的是音频缓冲区的长度(字节为单位)。其中的userdata参数是我们在定义SDL_AudioSpec结构体的时候指定的一般跟ffmpeg一起用的话,会实现为一个AVCodecContext结构体指针用于在回调函数的定义体中实现对音频数据包的解码。然后编程的时候我们来实现这个回调函数。具体的流程是:A 解码音频数据包 B 将解码后的音频数据向stream参数指向的音频缓冲区里面拷贝memcpy(),这样SDL内部自动会去实现音频的播放,这个应用程序员就不管了,程序员只管送进去就ok。
2 编程总体框架
SDL的音频编程需要进行回调播放,那么回调函数中的数据包从哪里来呢?当然是ffmpeg从文件里读入来的,但是,因为这里是回调函数,回调函数是运行在单独的线程中的,常用的做法是将ffmpeg从文件中取出来的数据AVPacket存在一个自定义的支持互斥访问的全局队列结构体中,这样两边的线程可以正常的互斥访问这个队列。在ffmpeg官方的tutorials中是定义了一个如下的队列结构体:
typedef struct PacketQueue {
AVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;
SDL_mutex *mutex;
SDL_cond *cond;
} PacketQueue;
其中的成员的AVPacketList类型是:
typedef struct AVPacketList {
AVPacket pkt;
struct AVPacketList *next;
} AVPacketList;
显然这个就是一个链表的节点而已,其中的AVPacket就是包数据了,但是,注意这个不是指针而是包本身。
显然,定义了数据结构自然还要定义相关的操作才行。简单地实现如下几类操作:
1) 队列初始化
void packet_queue_init(PacketQueue *q) {
memset(q, 0, sizeof(PacketQueue));
q->mutex = SDL_CreateMutex();
q->cond = SDL_CreateCond();
}
用到了SDL中的互斥锁和SDL的条件变量。
2) 音频数据包放入队列
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
AVPacketList *pkt1;
if(av_dup_packet(pkt) < 0) {
return -1;
}
pkt1 = av_malloc(sizeof(AVPacketList));
if (!pkt1)
return -1;
pkt1->pkt = *pkt;
pkt1->next = NULL;
SDL_LockMutex(q->mutex); //互斥访问
//这里插入的位置是在last_pkt之后,但是先要判断last_pkt是否为NULL
if (!q->last_pkt)
q->first_pkt = pkt1;
else
q->last_pkt->next = pkt1;
q->last_pkt = pkt1; // last_pkt下移
q->nb_packets++;
q->size += pkt1->pkt.size;
SDL_CondSignal(q->cond); //restart那个因为wait这个条件变量的进程
SDL_UnlockMutex(q->mutex);
return 0;
}
3) 从队列中取出音频数据包
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block)
{
AVPacketList *pkt1;
int ret;
SDL_LockMutex(q->mutex); //互斥访问队列
for(;;) {
if(quit) { // quit是全局变量,用于退出
ret = -1;
break;
}
pkt1 = q->first_pkt; // 显然每次取包都是取的队头的包
if (pkt1) { //如果队列中对头不为空的话
q->first_pkt = pkt1->next; //队头指针下移
if (!q->first_pkt) //如果取得的是最后一个包的话,那么记得设置下last_pkt,否则它会错指向刚才已经取走的包
q->last_pkt = NULL;
q->nb_packets--; //队列中的包数减少
q->size -= pkt1->pkt.size;
*pkt = pkt1->pkt; //由此可以看书,这个函数的pkt参数必须是有内存的,不能为一个指针。
av_free(pkt1); //取出的这个队列节点已经不用了,必须释放掉,否则就内存泄漏了,因为他是在前面的put里av_malloc来的
ret = 1;
break; //取完包,且成功了,返回1
} else if (!block) { //这里表示,队列为空,block是阻塞标志,为0表示不阻,立即返回0
ret = 0;
break;
} else {
SDL_CondWait(q->cond, q->mutex); // 在这里阻塞
}
}
SDL_UnlockMutex(q->mutex);
return ret;
}