在使用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
那么其中的callback函数什么时候被谁调用呢?
#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;
}