ffmpeg 和 SDL的教程 tutorial3学习--播放声音

时间:2020-12-03 12:05:16

tutorial3学习

现在我们要播放音频。SDL也停工了输出声音的方法。SDL_OpenAudio 函数就是用来打开音频设备的。他有一个结构体位SDL_AudioSpec的参数 ,这个参数包含了我们要输出的音频的所有信息。
在告诉你如何设置之前,我们先解释一下音频是如何被计算机处理的。数字音频包含了采样的流,每个采样代表了一个音频波的值。声音以某种采样率来录制,这说明了播放每个采样需要多快的速度,以每秒播放多少个采样来计算。例如常见的采样率是22050 和 44100,分别用于radio 和CD。此外,大多数音频有几个channel,这样可以有立体声或者环绕音,例如,如果采样时立体声,采样数据应该是每次得到两个。当我们从电影文件中得到数据时,我们不知道要得到多少采样,但是ffmpeg不会给我们部分采样---这也意味着它不会把立体声采样分开。

SDL播放音频的方法如下:设置音频选项:采样率(叫做freq),通道数量,等等。同时设置一个 回调函数和userdata。当我们开始播放音频时,SDL将持续的调用回调函数,且用一些字节来填充声音buffer。把这些信息放到SDL_AudioSpec结构体后,调用SDL_OpenAudio(), 这将打开音频设备,给我们另外一个AudioSpec结构体。这是我们使用的步骤---我么不能保证得到了所需的。

#####设置音频
请先记住这些概念,因为我们到目前为止还没有音频流的任何信息。让我们回到代码,我们将找到视频流和音频流。

//Find the first video stream
videoStream = -1;
audioStream = -1
for(i=0; i<pFormatCtx->nb_streams; i++)
{
    if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && videoStream<0)
{
    videoStream = i;
}
if(pFormatCtx->stream[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO &&audioStream<0)
{
    audioStream = i;
}
}
if(videoStream == -1)
return -1;
if(audioStream == -1)
return -1;
从这里我们 能够从流中通过AVCodecContex结构体得到所有的信息,就像之前处理视频时候。
AVCodecContext *aCodecCtxOrig;
AVCodecContext *aCodecCtx;
aCodecCtxOrig = pFormatCtx->streams[audioStream]->codec;
如果你还记得上个例子,我们将要自己 打开audio codec。下面是直截了当的:
AVCodec *aCodec;
aCodec = avcodec_find_decoder(aCodecCtxOrig->codec_id);
if(!aCodec)
{
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
//Copy context
aCodecCtx = avcodec_alloc_context3(aCodec);
if(avcodec_copy_context(aCodecCtx, aCodecCtxOrig) != 0)
{
fprintf(stderr, "Couldn't copy codec context");
return -1;
}
/* set up SDL Audio here*/
avcodec_open2(aCodecCtx, aCodec, NULL);
context中包含的信息就是我们设置音频是需要的信息。

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)<0(
{
    fprintf(stderr, "SDL_OpenAudio:%s\n", SDL_GetError());
}
我们来解释一下:
freq: 采样率
format:这告诉SDL我们将使用什么格式。S16SYS中的第一个S表示“signed"(有符号),16表示每个采样有16位,SYS表示大小端顺序依赖系统。这就是 avcodec_decode_audio2告诉我们进来的音频的格式。
channels: 音频通道的数量
silence :这是表示是否静音。因为音频是signed,0就是默认值。
samples:这个表示当SDL需要更多音频时,我们要SDL分配的buffer的大小。一个比较好的值是512和8192之间的值。ffplay用1024.
callback这里我们传递真正的回调函数。下面再详细讨论。
userdata:SDL给我们的回调函数一个纸箱userdata的void指针,这个数据时我们的回调函数需要的。我们要让它知道我们的codec context;
最后我们用  SDL_OpenAudio 打开音频。

队列:
这里我们准好了从stream中拉取音频信息。但是我们用这些信息做什么呢?我们将持续不断的从movie文件中获取packets,同时SDL将调用回调函数。这种解决方案将创建一些用来存放audio packets的全局结构体,所以 audio_callback需要从audio data中获取某些信息。所以我们要做的就是create一个 packets的队列。ffmpeg提供了这样一个结构体来帮助我们:AVPacketList,这是一个packets的链表。下面是队列的结构体。
typedef struct PacketQueue
{
    AVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;
SDL_mutext *mutex;
SDL_cond *cond;
}PacketQueue;

首先,需要指出 nb_packets 和 size 不同。size表示我们从packet_size中得到的字节大小。你会注意到我们有一个mutex 和 condition变量。这是因为SDL在一个单独的线程里面处理音频。如果不正确的lock住队列,我们会弄混数据。我们在处理队列的时候会看到。每个人都应该知道怎样make a queue,但是我们仍旧写下来让大家学习SDL函数。
首先,写一个函数初始化queue:
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 锁住了队列中的mutex,这样我们就能往队列中添加东西了。SDL_CondSignal()给函数发送了一个信号,告诉它有了数据,可以处理,然后unlock mutex,继续。
下面是相应的get 函数,注意SDL_CondWait,使得函数阻塞(例如在这里暂停直到获得数据)。
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 get;
}
就像你看到的,我们在函数里有个for无限循环,所以能够如果想阻塞的话确保得到数据。我们使用SDL_CondWait来避免死循环。基本来说,CondWait做的就是等待SDL_CondSignal()发送一个信号(或者SDL_CondBroadcast),然后继续。然而,看起来像是在mutex中获取--如果我们拿到lock,我们的put函数不能往队列中put任何数据。然而,SDL_CondWait做的就是unlock这个mutex,然后试图锁住它,当我们再次得到信号的时候。

你会注意到我们有一个quit变量,我们用来没有给程序设置一个退出信号。SDL自动的处理了TERM信号。否则,线程将会无限循环下去。
SDL_PollEvent(&event);
switch(event.type)
{
case SDL_QUIT:
quit = 1;
}
我们保证把quit置为1;

填充packet
剩下的事情就是建立我们的queue

PacketQueue audioq;

main()
{
......
avcodec_open2(aCodecCtx, aCodec, NULL);
packet_queue_init(&audioq);
SDL_PauseAudio(0);
}

SDL_PauseAudio 启动音频设备。如果没有得到数据,就播放静音。它不会马上。

到这里,我们建立了queue,现在我们开始填充packet。我们到packet的读循环去:

while(av_read_frame(pFormatCtx, &packet) >=0)
{
//Is this  a packet from the video stream?
if(packet.stream_index == videoStream)
{
//Decode video frame
}
if(packet.stream_index == audioStream)
{
packet_queue_put(&audioq, &packet);
}
else
{
av_free_packet(&packet);
}
}
注意到我们把packet放到队列中后没有马上free,我们在decode之后才去free。

### 得到packet
让我们的 audio_callback函数来获取queue上的packets。回调函数必须格式为 
void callback(void *userdata, Uint8 *stream, int len)
userdata 是我们给SDL 的指针,stream是我们把audio data 写入的指针,len是buffer的长度。下面是代码

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)
{
//We have already sent all our data; get more
audio_size = audio_decode_frame(aCodecCtx, audio_buf, sizeof(audio_buf));
if(audio_size < 0)
{
//if error, output silence
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>1en)
len1 = len;
memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
len -= len1;
stream ==len1;
audio_buf_index +=len1;
}
}
这是一个简单的循环,从另外一个function中获取data, audio_decode_frame, 把结果存储在一个临时buffer中,试图 写len长度个字节到stream中,并且当没有足够数据时get more data。或者当有多余的data时把它保存起来。audio_buf的大小是audio_frame的1.5倍,这给了我们一个好的缓冲。

### 最后解码音频
让我们看看 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;
      static AVFrame frame;

      int len1, data_size = 0;

      for(;;) {
        while(audio_pkt_size > 0) {
          int got_frame = 0;
          len1 = avcodec_decode_audio4(aCodecCtx, &frame;, &got;_frame, &pkt;);
          if(len1 < 0) {
        /* if error, skip frame */
        audio_pkt_size = 0;
        break;
          }
          audio_pkt_data += len1;
          audio_pkt_size -= len1;
          data_size = 0;
          if(got_frame) {
        data_size = av_samples_get_buffer_size(NULL, 
                               aCodecCtx->channels,
                               frame.nb_samples,
                               aCodecCtx->sample_fmt,
                               1);
        assert(data_size <= buf_size);
        memcpy(audio_buf, frame.data[0], data_size);
          }
          if(data_size <= 0) {
        /* No data yet, get more frames */
        continue;
          }
          /* We have data, return it and come back for more later */
          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 函数。我们从queue中获取数据,保存信息。然后,一旦有了可以处理的packet,就调用avcodec_decode_audio4(),这个和 avcodec_decode_video()功能类似,区别在于,packet可能有多帧,这样,你必须多次调用以得到所有的数据。一旦有了frame,我们简单的把它copy到audio buffer,保证 data_size 比 audio buffer小。并且记住 audio_buf的大小,因为SDL处理8bit,ffmpeg处理16bit的int。也应该注意len1和data_size的区别,len1是我们使用了的packet,data_size是返回的raw data的数量。

当我们有了一些data后,我们立刻返回去看是否仍旧需要得到跟多的data,或者已经结束。如果有更多的packet要处理,我们先保存下来。如果处理完一个packet,我们把它free。

这就是整个过程。我们从主循环中得到audio然后放到queue中,然后audio_callback函数从这个队列中读,这个回调函数交给sdl来处理,SDL把声音送给声卡。

现在好了,视频还是很快播放,但是音频正常。为什么

我们需要重新组织一下代码,这种从queue里面获取audio并且在单独的线程里面播放工作很好:这使得代码更好管理和更模块化。在同步音频和视频之前,我们要使的代码更容易处理。
下次:产生线程。