关于《最简单的基于FFMPEG+SDL的音频播放器》记录

时间:2021-08-06 12:11:55

一、概述

之前的最简单的基于FFMPEG+SDL的视频播发器记录中,我们实现了播放视频的功能,但是还不能播放声音,这次我们就将实现声音的播放。为了减小难度,先只做一个音频播放器。在后续的文章中会在视频播放器中加入音频播放的功能。
同理,音频播放器的实现可以参考原作者的文章最简单的基于FFMPEG+SDL的音频播放器,这里仅仅记录自己的心得。

二、音频播放器流程

1、解码流程       

音频的解码流程与视频的解码流程是类似的,只是解码音频的函数是:avcodec_decode_audio4,其余的就不再赘述,流程如下图所示:

关于《最简单的基于FFMPEG+SDL的音频播放器》记录


2、SDL播放流程

SDL处理的大致流程如下图所示:

关于《最简单的基于FFMPEG+SDL的音频播放器》记录

接下来对各个流程做一下简单的解释:

1)SDL_Init    

初始化SDL库

2)设置输出音频参数

       这一步我们将设置输出音频参数,例如:采样率、声道数、音频数据格式等等。所有的这些参数都在SDL_AudioSpec这个结构体中,SDL_AudioSpec的结构如下:

         

数据类型

属性

                                                                    说明

int

freq

采样率,通常有11025, 22050, 44100 and 48000。数字越大,音质越好。

SDL_AudioFormat

format

音频数据格式,即每个采样的大小和类型,例如:AUDIO_S16SYS表示有符号16位

Uint8

channels

声道数,SDL2.0支持1(mono),2(stereo),4(quad),6(5.1声道)四种值,声道数越多,立体效果越好。

Uint8

silence

静音值,例如用0代表静音。

Uint16

samples

音频缓冲大小 (power of 2),ffplay为1024

Uint32

size

音频缓冲大小,以字节为单位 

SDL_AudioCallback

callback

播放音频时的回调函数,参数为:userdata(编解码上下文),stream(音频数据),len(数据大小)

void*

userdata

回调函数需要的参数,一般为编解码上下文

    其中声道数可以通过如下代码获取:

      //声道布局
//uint64_t out_channel_layout = AV_CH_LAYOUT_STEREO;//立体声 2
uint64_t out_channel_layout = AV_CH_LAYOUT_MONO;//单声道 1
int out_channels = av_get_channel_layout_nb_channels(out_channel_layout);//根据通道布局类型获取通道数
一个设置输出音频参数的例子:

        SDL_AudioSpec wanted_spec;
wanted_spec.freq = 44100;//CD音质
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = out_channels;
wanted_spec.silence = 0;
wanted_spec.samples = 1024;
wanted_spec.callback = fill_audio;//需要自己实现它
wanted_spec.userdata = pCodecCtx_audio;//用户数据,它将提供给回调函数使用,这里即编解码上下文

其中fill_audio的实现如下:

/* The audio function callback takes the following parameters:
* stream: A pointer to the audio buffer to be filled
* len: The length (in bytes) of the audio buffer
*/
void fill_audio(void *udata, Uint8 *stream, int len){
//SDL 2.0
SDL_memset(stream, 0, len);
if (audio_len == 0) /* Only play if we have data left */
return;
len = (len>audio_len ? audio_len : len); /* Mix as much data as possible */

SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME);
audio_pos += len;
audio_len -= len;
}

3)SDL_OpenAudio

      打开音频设备。该函数原型如下:

int SDL_OpenAudio(SDL_AudioSpec* desired, SDL_AudioSpec* obtained)

  参数:

desired

期望输出的格式

obtained

实际得到的格式,可以手动设置为NULL

这个函数以期望的格式打开音频设备,并将实际的硬件参数填充到obtained中。如果obtained是NULL,那么必须保证传递给回调函数的音频数据必须是期望的格式,而且必要时会自动转换成实际的音频格式。如果obtained是NULL,desired会修改字段的值。这就是为什么我们后面必须对音频数据格式进行转换。

此外SDL_OpenAudio默认打开的是1号音频设备,如果想指定设备,请使用SDL_OpenAudioDevice。

4) swr_alloc_set_opts

设置转换格式的参数。代码如下:        

//获取声道布局
pCodecCtx_audio->channel_layout = av_get_default_channel_layout(pCodecCtx_audio->channels);
//Swr  
struct SwrContext *au_convert_ctx;
au_convert_ctx = swr_alloc();
au_convert_ctx = swr_alloc_set_opts(au_convert_ctx, out_channel_layout, out_sample_fmt, out_sample_rate,
pCodecCtx_audio->channel_layout, pCodecCtx_audio->sample_fmt, pCodecCtx_audio->sample_rate, 0, NULL);

5)swr_init

     初始化SWR上下文。

6)swr_convert

     对解码后的数据帧进行数据格式转换。

7)SDL_PauseAudio

      播放音频。

三、源代码

开发环境:VS 2013

#include <stdio.h>
#include<stdlib.h>
#include<string.h>

//包含库
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "SDL2/SDL.h"
#include "libswresample/swresample.h"
};

#define MAX_AUDIO_FRAME_SIZE 192000 // 1 second of 48khz 32bit audio
//音频相关
//Buffer:
//|-----------|-------------|
//chunk-------pos---len-----|
static Uint8 *audio_chunk;
static Uint32 audio_len;
static Uint8 *audio_pos;
/* The audio function callback takes the following parameters:
* stream: A pointer to the audio buffer to be filled
* len: The length (in bytes) of the audio buffer
*/
void fill_audio(void *udata, Uint8 *stream, int len){
//SDL 2.0
SDL_memset(stream, 0, len);
if (audio_len == 0) /* Only play if we have data left */
return;
len = (len>audio_len ? audio_len : len); /* Mix as much data as possible */

SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME);
audio_pos += len;
audio_len -= len;
}


int main(int argc, char* argv[])
{
//FFmpeg相关变量
AVFormatContext *pFormatCtx;//AVFormatContext主要存储视音频封装格式中包含的信息
unsigned i;
int audioindex;//音频流所在序号
AVCodecContext *pCodecCtx_audio;//AVCodecContext,存储该音频流使用解码方式的相关数据
AVCodec *pCodec_audio;//音频解码器
AVFrame *pFrame_audio;
AVPacket packet;//解码前数据

char* filepath = "1.mp3";//输入文件


av_register_all();//初始化libformat库和注册编解码器
avformat_network_init();//初始化网络组件
pFormatCtx = avformat_alloc_context();
//打开视频文件然后读取头部信息到pFormatCtx
if (avformat_open_input(&pFormatCtx, filepath, NULL, NULL) != 0){
printf("Couldn't open input stream.\n");
return -1;
}
//获取流信息
if (avformat_find_stream_info(pFormatCtx, NULL) < 0){
printf("Couldn't find stream information.\n");
return -1;
}
audioindex = -1;
//找到音频流的序号
for (i = 0; i < pFormatCtx->nb_streams; i++)
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO){
audioindex = i;
break;
}
if (audioindex == -1){
printf("Didn't find a audio stream.\n");
return -1;
}

//音频
pCodecCtx_audio = pFormatCtx->streams[audioindex]->codec;//获取解码环境
pCodec_audio = avcodec_find_decoder(pCodecCtx_audio->codec_id);//获取解码器
if (pCodec_audio == NULL){
printf("Codec not found.\n");
return -1;
}
//打开解码器
if (avcodec_open2(pCodecCtx_audio, pCodec_audio, NULL) < 0){
printf("Could not open codec.\n");
return -1;
}

//设置输出的音频参数
int out_nb_samples = 1024;//单个通道样本个数
AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16;//采样格式
int out_sample_rate = 44100;//输出时采样率,CD一般为44100HZ
//声道布局
//uint64_t out_channel_layout = AV_CH_LAYOUT_STEREO;//立体声
//uint64_t out_channel_layout = AV_CH_LAYOUT_MONO;//单声道
uint64_t out_channel_layout = AV_CH_LAYOUT_5POINT1;
int out_channels = av_get_channel_layout_nb_channels(out_channel_layout);//根据通道布局类型获取通道数
//根据通道数、样本个数、采样格式分配内存
int out_buffer_size = av_samples_get_buffer_size(NULL, out_channels, out_nb_samples, out_sample_fmt, 1);
uint8_t *out_buffer_audio = (uint8_t *)av_malloc(MAX_AUDIO_FRAME_SIZE * 2);//*2是保证输出缓存大于输入数据大小


pFrame_audio = av_frame_alloc();

//SDL Begin----------------------------
//初始化SDL库
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
printf("Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}

//音频部分
//设置输出参数
//SDL_AudioSpec
//包含了我们将要输出的音频的所有信息。
SDL_AudioSpec wanted_spec;
wanted_spec.freq = out_sample_rate;//采样率,即播放速度
wanted_spec.format = AUDIO_S16SYS;//音频数据格式。在“S16SYS”中的S 表示有符号的signed,
//16 表示每个样本是16 位长的,SYS 表示大小头的顺序是与使用的系统相同的。
//这些格式是由 avcodec_decode_audio 为我们给出来的输入音频的格式
wanted_spec.channels = out_channels;//声道数
wanted_spec.silence = 0; //静音值,因为数据是有符号数,所以0表示静音
wanted_spec.samples = out_nb_samples;//这是当我们想要更多声音的时候,我们想让SDL 给出来的声音缓冲
//区的尺寸。一个比较合适的值在512 到8192 之间;ffplay 使用1024。
wanted_spec.callback = fill_audio;//回调函数,向声音缓冲out_buffer_audio填入一个特定的数量的字节。
wanted_spec.userdata = pCodecCtx_audio;//用户数据,它将提供给回调函数使用,这里即编解码上下文
//打开音频
if (SDL_OpenAudio(&wanted_spec, NULL)<0){
printf("can't open audio.\n");
return -1;
}
//SDL End------------------------
//音频
uint32_t len = 0;
int got_picture_audio;
int index = 0;

//获取声道布局
pCodecCtx_audio->channel_layout = av_get_default_channel_layout(pCodecCtx_audio->channels);
//Swr
struct SwrContext *au_convert_ctx;
au_convert_ctx = swr_alloc();
au_convert_ctx = swr_alloc_set_opts(au_convert_ctx, out_channel_layout, out_sample_fmt, out_sample_rate,
pCodecCtx_audio->channel_layout, pCodecCtx_audio->sample_fmt, pCodecCtx_audio->sample_rate, 0, NULL);
swr_init(au_convert_ctx);

while (av_read_frame(pFormatCtx, &packet) >= 0){//读取下一帧数据
if (packet.stream_index == audioindex)//如果是音频流帧
{
if (avcodec_decode_audio4(pCodecCtx_audio, pFrame_audio, &got_picture_audio, &packet) < 0) {
printf("Error in decoding audio frame.\n");
return -1;
}
if (got_picture_audio > 0){
swr_convert(au_convert_ctx, &out_buffer_audio, MAX_AUDIO_FRAME_SIZE, (const uint8_t **)pFrame_audio->data, pFrame_audio->nb_samples);
//FIX:FLAC,MP3,AAC Different number of samples
if (wanted_spec.samples != pFrame_audio->nb_samples){
SDL_CloseAudio();
out_nb_samples = pFrame_audio->nb_samples;
out_buffer_size = av_samples_get_buffer_size(NULL, out_channels, out_nb_samples, out_sample_fmt, 1);
wanted_spec.samples = out_nb_samples;
SDL_OpenAudio(&wanted_spec, NULL);
}
//Set audio buffer (PCM data)
audio_chunk = (Uint8 *)out_buffer_audio;
//Audio buffer length
audio_len = out_buffer_size;
audio_pos = audio_chunk;
//Play
SDL_PauseAudio(0);
while (audio_len > 0)//Wait until finish
SDL_Delay(1);//ms
}
}
av_free_packet(&packet);
}

swr_free(&au_convert_ctx);
SDL_CloseAudio();//Close SDL
SDL_Quit();//退出SDL

av_free(out_buffer_audio);
av_frame_free(&pFrame_audio);

avcodec_close(pCodecCtx_audio);//释放解码器环境
avformat_close_input(&pFormatCtx);//释放输入环境
return 0;
}