一、概述
之前的最简单的基于FFMPEG+SDL的视频播发器记录中,我们实现了播放视频的功能,但是还不能播放声音,这次我们就将实现声音的播放。为了减小难度,先只做一个音频播放器。在后续的文章中会在视频播放器中加入音频播放的功能。
同理,音频播放器的实现可以参考原作者的文章最简单的基于FFMPEG+SDL的音频播放器,这里仅仅记录自己的心得。
二、音频播放器流程
1、解码流程
音频的解码流程与视频的解码流程是类似的,只是解码音频的函数是:avcodec_decode_audio4,其余的就不再赘述,流程如下图所示:
2、SDL播放流程
SDL处理的大致流程如下图所示:
接下来对各个流程做一下简单的解释:
1)SDL_Init
初始化SDL库
2)设置输出音频参数
这一步我们将设置输出音频参数,例如:采样率、声道数、音频数据格式等等。所有的这些参数都在SDL_AudioSpec这个结构体中,SDL_AudioSpec的结构如下:
数据类型 |
属性 |
说明 |
int |
freq |
采样率,通常有11025, 22050, 44100 and 48000。数字越大,音质越好。 |
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;
}