如今咱们要来播放声音。SDL也为咱们准备了输出声音的方法。函数SDL_OpenAudio()自己就是用来打开声音设备的。它使用一个叫作SDL_AudioSpec结构体做为参数,这个结构体中包含了咱们将要输出的音频的全部信息。程序员
在咱们展现如何创建以前,让咱们先解释一下电脑是如何处理音频的。数字音频是由一长串的样本流组成的。每一个样本表示声音波形中的一个值。声音按照一个特定的采样率来进行录制,采样率表示以多快的速度来播放这段样本流,它的表示方式为每秒多少次采样。例如22050和44100的采样率就是电台和CD经常使用的采样率。此外,大多音频有不仅一个通道来表示立体声或者环绕。例如,若是采样是立体声,那么每次的采样数就为2个。当咱们从一个电影文件中等到数据的时候,咱们不知道咱们将获得多少个样本,可是ffmpeg将不会给咱们部分的样本――这意味着它将不会把立体声分割开来。ide
SDL播放声音的方式是这样的:你先设置声音的选项:采样率(在SDL的结构体中被叫作freq的表示频率frequency),声音通道数和其它的参数,而后咱们设置一个回调函数和一些用户数据userdata。当开始播放音频的时候,SDL将不断地调用这个回调函数而且要求它来向声音缓冲填入一个特定的数量的字节。当咱们把这些信息放到SDL_AudioSpec结构体中后,咱们调用函数SDL_OpenAudio()就会打开声音设备而且给咱们送回另一个AudioSpec结构体。这个结构体是咱们实际上用到的--由于咱们不能保证获得咱们所要求的。模块化
设置音频函数
目前先把讲的记住,由于咱们实际上尚未任何关于声音流的信息。让咱们回过头来看一下咱们的代码,看咱们是如何找到视频流的,一样咱们也能够找到声音流。学习
// Find the first video streamui videoStream=-1;this audioStream=-1;url for(i=0; i < pFormatCtx->nb_streams; i++) {spa if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO线程 && videoStream < 0) { videoStream=i; } if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_AUDIO && audioStream < 0) { audioStream=i; } } if(videoStream==-1) return -1; // Didn't find a video stream if(audioStream==-1) return -1; |
从这里咱们能够从描述流的AVCodecContext中获得咱们想要的信息,就像咱们获得视频流的信息同样。
AVCodecContext *aCodecCtx;
aCodecCtx=pFormatCtx->streams[audioStream]->codec; |
包含在编解码上下文中的全部信息正是咱们所须要的用来创建音频的信息:
wanted_spec.freq = aCodecCtx->sample_rate; wanted_spec.format = AUDIO_S16SYS; wanted_spec.channels = aCodecCtx->channels; wanted_spec.silence = 0; wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE; wanted_spec.callback = audio_callback; wanted_spec.userdata = aCodecCtx;
if(SDL_OpenAudio(&wanted_spec, &spec) < 0) { fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError()); return -1; } |
让咱们浏览一下这些:
·freq 前面所讲的采样率
·format 告诉SDL咱们将要给的格式。在“S16SYS”中的S表示有符号的signed,16表示每一个样本是16位长的,SYS表示大小头的顺序是与使用的系统相同的。这些格式是由avcodec_decode_audio2为咱们给出来的输入音频的格式。
·channels 声音的通道数
·silence 这是用来表示静音的值。由于声音采样是有符号的,因此0固然就是这个值。
·samples 这是当咱们想要更多声音的时候,咱们想让SDL给出来的声音缓冲区的尺寸。一个比较合适的值在512到8192之间;ffplay使用1024。
·callback 这个是咱们的回调函数。咱们后面将会详细讨论。
·userdata 这个是SDL供给回调函数运行的参数。咱们将让回调函数获得整个编解码的上下文;你将在后面知道缘由。
最后,咱们使用SDL_OpenAudio函数来打开声音。
若是你还记得前面的指导,咱们仍然须要打开声音编解码器自己。这是很显然的。
AVCodec *aCodec;
aCodec = avcodec_find_decoder(aCodecCtx->codec_id); if(!aCodec) { fprintf(stderr, "Unsupported codec!\n"); return -1; } avcodec_open(aCodecCtx, aCodec); |
队列
嗯!如今咱们已经准备好从流中取出声音信息。可是咱们如何来处理这些信息呢?咱们将会不断地从文件中获得这些包,但同时SDL也将调用回调函数。解决方法为建立一个全局的结构体变量以便于咱们从文件中获得的声音包有地方存放同时也保证SDL中的声音回调函数audio_callback能从这个地方获得声音数据。因此咱们要作的是建立一个包的队列queue。在ffmpeg中有一个叫AVPacketList的结构体能够帮助咱们,这个结构体实际是一串包的链表。下面就是咱们的队列结构体:
typedef struct PacketQueue { AVPacketList *first_pkt, *last_pkt; int nb_packets; int size; SDL_mutex *mutex; SDL_cond *cond; } PacketQueue; |
首先,咱们应当指出nb_packets是与size不同的--size表示咱们从packet->size中获得的字节数。你会注意到咱们有一个互斥量mutex和一个条件变量cond在结构体里面。这是由于SDL是在一个独立的线程中来进行音频处理的。若是咱们没有正确的锁定这个队列,咱们有可能把数据搞乱。咱们未来看一个这个队列是如何来运行的。每个程序员应当知道如何来生成的一个队列,可是咱们将把这部分也来讨论从而能够学习到SDL的函数。
一开始咱们先建立一个函数来初始化队列:
void packet_queue_init(PacketQueue *q) { memset(q, 0, sizeof(PacketQueue)); q->mutex = SDL_CreateMutex(); q->cond = SDL_CreateCond(); } |
接着咱们再作一个函数来给队列中填入东西:
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);
if (!q->last_pkt) q->first_pkt = pkt1; else q->last_pkt->next = pkt1; q->last_pkt = pkt1; q->nb_packets++; q->size += pkt1->pkt.size; SDL_CondSignal(q->cond);
SDL_UnlockMutex(q->mutex); return 0; } |
函数SDL_LockMutex()锁定队列的互斥量以便于咱们向队列中添加东西,而后函数SDL_CondSignal()经过咱们的条件变量为一个接收函数(若是它在等待)发出一个信号来告诉它如今已经有数据了,接着就会解锁互斥量并让队列能够自由访问。
下面是相应的接收函数。注意函数SDL_CondWait()是如何按照咱们的要求让函数阻塞block的(例如一直等到队列中有数据)。
int quit = 0;
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) { AVPacketList *pkt1; int ret;
SDL_LockMutex(q->mutex);
for(;;) {
if(quit) { ret = -1; break; }
pkt1 = q->first_pkt; if (pkt1) { q->first_pkt = pkt1->next; if (!q->first_pkt) q->last_pkt = NULL; q->nb_packets--; q->size -= pkt1->pkt.size; *pkt = pkt1->pkt; av_free(pkt1); ret = 1; break; } else if (!block) { ret = 0; break; } else { SDL_CondWait(q->cond, q->mutex); } } SDL_UnlockMutex(q->mutex); return ret; } |
正如你所看到的,咱们已经用一个无限循环包装了这个函数以便于咱们想用阻塞的方式来获得数据。咱们经过使用SDL中的函数SDL_CondWait()来避免无限循环。基本上,全部的CondWait只等待从SDL_CondSignal()函数(或者SDL_CondBroadcast()函数)中发出的信号,而后再继续执行。然而,虽然看起来咱们陷入了咱们的互斥体中--若是咱们一直保持着这个锁,咱们的函数将永远没法把数据放入到队列中去!可是,SDL_CondWait()函数也为咱们作了解锁互斥量的动做而后才尝试着在获得信号后去从新锁定它。
意外状况
大家将会注意到咱们有一个全局变量quit,咱们用它来保证尚未设置程序退出的信号(SDL会自动处理TERM相似的信号)。不然,这个线程将不停地运行直到咱们使用kill -9来结束程序。FFMPEG一样也提供了一个函数来进行回调并检查咱们是否须要退出一些被阻塞的函数:这个函数就是url_set_interrupt_cb。
int decode_interrupt_cb(void) { return quit; } ... main() { ... url_set_interrupt_cb(decode_interrupt_cb); ... SDL_PollEvent(&event); switch(event.type) { case SDL_QUIT: quit = 1; ... |
固然,这仅仅是用来给ffmpeg中的阻塞状况使用的,而不是SDL中的。咱们还必须要设置quit标志为1。
为队列提供包
剩下的咱们惟一须要为队列所作的事就是提供包了:
PacketQueue audioq; main() { ... avcodec_open(aCodecCtx, aCodec);
packet_queue_init(&audioq); SDL_PauseAudio(0); |
函数SDL_PauseAudio()让音频设备最终开始工做。若是没有当即供给足够的数据,它会播放静音。
咱们已经创建好咱们的队列,如今咱们准备为它提供包。先看一下咱们的读取包的循环:
while(av_read_frame(pFormatCtx, &packet)>=0) { // Is this a packet from the video stream? if(packet.stream_index==videoStream) { // Decode video frame .... } } else if(packet.stream_index==audioStream) { packet_queue_put(&audioq, &packet); } else { av_free_packet(&packet); } |
注意:咱们没有在把包放到队列里的时候释放它,咱们将在解码后来释放它。
取出包
如今,让咱们最后让声音回调函数audio_callback来从队列中取出包。回调函数的格式必需为void callback(void *userdata, Uint8 *stream, int len),这里的userdata就是咱们给到SDL的指针,stream是咱们要把声音数据写入的缓冲区指针,len是缓冲区的大小。下面就是代码:
void audio_callback(void *userdata, Uint8 *stream, int len) {
AVCodecContext *aCodecCtx = (AVCodecContext *)userdata; int len1, audio_size;
static uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2]; static unsigned int audio_buf_size = 0; static unsigned int audio_buf_index = 0;
while(len > 0) { if(audio_buf_index >= audio_buf_size) {
audio_size = audio_decode_frame(aCodecCtx, audio_buf, sizeof(audio_buf)); if(audio_size < 0) {
audio_buf_size = 1024; memset(audio_buf, 0, audio_buf_size); } else { audio_buf_size = audio_size; } audio_buf_index = 0; } len1 = audio_buf_size - audio_buf_index; if(len1 > len) len1 = len; memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1); len -= len1; stream += len1; audio_buf_index += len1; } } |
这基本上是一个简单的从另一个咱们将要写的audio_decode_frame()函数中获取数据的循环,这个循环把结果写入到中间缓冲区,尝试着向流中写入len字节而且在咱们没有足够的数据的时候会获取更多的数据或者当咱们有多余数据的时候保存下来为后面使用。这个audio_buf的大小为1.5倍的声音帧的大小以便于有一个比较好的缓冲,这个声音帧的大小是ffmpeg给出的。
最后解码音频
让咱们看一下解码器的真正部分:audio_decode_frame
int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf, int buf_size) {
static AVPacket pkt; static uint8_t *audio_pkt_data = NULL; static int audio_pkt_size = 0;
int len1, data_size;
for(;;) { while(audio_pkt_size > 0) { data_size = buf_size; len1 = avcodec_decode_audio2(aCodecCtx, (int16_t *)audio_buf, &data_size, audio_pkt_data, audio_pkt_size); if(len1 < 0) {
audio_pkt_size = 0; break; } audio_pkt_data += len1; audio_pkt_size -= len1; if(data_size <= 0) {
continue; }
return data_size; } if(pkt.data) av_free_packet(&pkt);
if(quit) { return -1; }
if(packet_queue_get(&audioq, &pkt, 1) < 0) { return -1; } audio_pkt_data = pkt.data; audio_pkt_size = pkt.size; } } |
整个过程实际上从函数的尾部开始,在这里咱们调用了packet_queue_get()函数。咱们从队列中取出包,而且保存它的信息。而后,一旦咱们有了可使用的包,咱们就调用函数avcodec_decode_audio2(),它的功能就像它的姐妹函数avcodec_decode_video()同样,惟一的区别是它的一个包里可能有不止一个声音帧,因此你可能要调用不少次来解码出包中全部的数据。同时也要记住进行指针audio_buf的强制转换,由于SDL给出的是8位整型缓冲指针而ffmpeg给出的数据是16位的整型指针。你应该也会注意到len1和data_size的不一样,len1表示解码使用的数据的在包中的大小,data_size表示实际返回的原始声音数据的大小。
当咱们获得一些数据的时候,咱们马上返回来看一下是否仍然须要从队列中获得更加多的数据或者咱们已经完成了。若是咱们仍然有更加多的数据要处理,咱们把它保存到下一次。若是咱们完成了一个包的处理,咱们最后要释放它。
就是这样。咱们利用主的读取队列循环从文件获得音频并送到队列中,而后被audio_callback函数从队列中读取并处理,最后把数据送给SDL,因而SDL就至关于咱们的声卡。让咱们继续而且编译:
gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lz -lm \ `sdl-config --cflags --libs` |
啊哈!视频虽然仍是像原来那样快,可是声音能够正常播放了。这是为何呢?由于声音信息中的采样率--虽然咱们把声音数据尽量快的填充到声卡缓冲中,可是声音设备却会按照原来指定的采样率来进行播放。
咱们几乎已经准备好来开始同步音频和视频了,可是首先咱们须要的是一点程序的组织。用队列的方式来组织和播放音频在一个独立的线程中工做的很好:它使得程序更加更加易于控制和模块化。在咱们开始同步音视频以前,咱们须要让咱们的代码更加容易处理。因此下次要讲的是:建立一个线程。