使用ffmpeg解码音频并使用sdl播放,网上还是有一些例子的,大多都不是特别完善,比如打开音频设备、音频重采样、使用push的方式播放音频等,都是有不少细节需要注意处理。尤其是使用push的方式播放音频,流程很简单完全可以使用单线程实现,但是队列数据长度比较难控制控制。而且有时想要快速搭建一个demo时,总是要重新编写不少代码,比较不方便,所以在这里提供一个完善的例子,可以直接拷贝拿来使用。
ffmpeg解码的流程是比较经典且通用的,基本上是文件、网络流、本地设备都是一模一样的流程。
首先需要打开输入流,输入流可以是文件、rtmp、rtsp、http等。
AVFormatContext* pFormatCtx = NULL;
const char* input="test.mp4";
//打开输入流
avformat_open_input(&pFormatCtx, input, NULL, NULL) ;
//查找输入流信息
avformat_find_stream_info(pFormatCtx, NULL) ;
因为是音频播放,所以需要找到输入流中的音频流。通过遍历判断codec_type 为AVMEDIA_TYPE_AUDIO值的流。音频流有可能有多个的,这里我们取第一个。
//视频流的下标
int audioindex = -1;
for (unsigned i = 0; i < pFormatCtx->nb_streams; i++)
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
audioindex = i;
break;
}
通过输入流的信息获取到解码器参数然后查找到响应解码器,最后打开解码器即可。
AVCodecContext* pCodecCtx = NULL;
const AVCodec* pCodec = NULL;
//初始化解码上下文
pCodecCtx=avcodec_alloc_context3(NULL);
//获取解码参数
avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[audioindex]->codecpar)
//查找解码器
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
//打开解码器
avcodec_open2(pCodecCtx, pCodec, &opts)
打开解码器时可以使用多线程参数优化解码速度。
AVDictionary* opts = NULL;
//使用多线程解码
if (!av_dict_get(opts, "threads", NULL, 0))
av_dict_set(&opts, "threads", "auto", 0);
解码的流程就是读取输入流的包,对包进行解码,获取解码后的帧。
AVPacket packet;
AVFrame* pFrame = av_frame_alloc();
//读取包
while (av_read_frame(pFormatCtx, &packet) == 0)
{ //发送包
avcodec_send_packet(pCodecCtx, &packet);
//接收帧
while (avcodec_receive_frame(pCodecCtx, pFrame) == 0)
{
//取得解码后的帧pFrame
av_frame_unref(pFrame);
}
av_packet_unref(&packet);
}
解码有个细节是需要注意的,即当av_read_frame到文件尾结束后,需要再次调用avcodec_send_packet传入NULL或者空包flush出里面的缓存帧。下面是完善的解码流程:
while (1)
{
int gotPacket = av_read_frame(pFormatCtx, &packet) == 0;
if (!gotPacket || packet.stream_index == audioindex)
//!gotPacket:未获取到packet需要将解码器的缓存flush,所以还需要进一次解码流程。
{
//发送包
if (avcodec_send_packet(pCodecCtx, &packet) < 0)
{
printf("Decode error.\n");
av_packet_unref(&packet);
goto end;
}
//接收解码的帧
while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) {
//取得解码后的帧pFrame
av_frame_unref(pFrame);
}
}
av_packet_unref(&packet);
if (!gotPacket)
break;
}
当遇到音频格式或采样率、声道数与输出目标不一致时,就需要进行重采样了,重采样通常放在解码循环中。
struct SwrContext* swr_ctx = NULL;
enum AVSampleFormatforceFormat = AV_SAMPLE_FMT_FLT;
uint8_t* data;
size_t dataSize;
if (forceFormat != pCodecCtx->sample_fmt|| spec.freq!= pFrame->sample_rate|| spec.channels!= pFrame->channels)
//重采样
{
//计算输入采样数
int out_count = (int64_t)pFrame->nb_samples * spec.freq / pFrame->sample_rate + 256;
//计算输出数据大小
int out_size = av_samples_get_buffer_size(NULL, spec.channels, out_count, forceFormat, 0);
//输入数据指针
const uint8_t** in = (const uint8_t**)pFrame->extended_data;
//输出缓冲区指针
uint8_t** out = &outBuffer;
int len2 = 0;
if (out_size < 0) {
av_log(NULL, AV_LOG_ERROR, "av_samples_get_buffer_size() failed\n");
goto end;
}
if (!swr_ctx)
//初始化重采样对象
{
swr_ctx = swr_alloc_set_opts(NULL, av_get_default_channel_layout(spec.channels), forceFormat, spec.freq, pCodecCtx->channel_layout, pCodecCtx->sample_fmt, pCodecCtx->sample_
if (!swr_ctx|| swr_init(swr_ctx) < 0) {
av_log(NULL, AV_LOG_ERROR, "swr_alloc_set_opts() failed\n");
goto end;
}
}
if (!outBuffer)
//申请输出缓冲区
{
outBuffer = (uint8_t*)av_mallocz(out_size);
}
//执行重采样
len2 = swr_convert(swr_ctx, out, out_count, in, pFrame->nb_samples);
if (len2 < 0) {
av_log(NULL, AV_LOG_ERROR, "swr_convert() failed\n");
goto end;
}
//取得输出数据
data = outBuffer;
//输出数据长度
dataSize = av_samples_get_buffer_size(0, spec.channels, len2, forceFormat, 1);
}
使用完成后需要释放资源。
//销毁资源
if (pFrame)
{
if (pFrame->format != -1)
{
av_frame_unref(pFrame);
}
av_frame_free(&pFrame);
}
if (packet.data)
{
av_packet_unref(&packet);
}
if (pCodecCtx)
{
avcodec_close(pCodecCtx);
avcodec_free_context(&pCodecCtx);
}
if (pFormatCtx)
avformat_close_input(&pFormatCtx);
if (pFormatCtx)
avformat_free_context(pFormatCtx);
swr_free(&swr_ctx);
av_dict_free(&opts);
if (outBuffer)
av_free(outBuffer);
使用sdl前需要在最开始初始化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_OpenAudioDevice打开设备,使用SDL_OpenAudio的话samples设置可能不生效,不利于push的方式播放。
SDL_AudioSpec wanted_spec, spec;
int audioId = 0;
//打开设备
wanted_spec.channels = av_get_channel_layout_nb_channels(pCodecCtx->channel_layout);
wanted_spec.freq = pCodecCtx->sample_rate;
wanted_spec.format = AUDIO_F32SYS;
wanted_spec.silence = 0;
wanted_spec.samples = FFMAX(512, 2 << av_log2(wanted_spec.freq / 30));
wanted_spec.callback = NULL;
wanted_spec.userdata = NULL;
audioId = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec, 1);
if (audioId < 2)
{
printf("Open audio device error!\n");
goto end;
}
//开启播放
SDL_PauseAudioDevice(audioId, 0);
我们采用push的方式播放,即调用SDL_QueueAudio,将音频数据写入sdl内部维护的队列中,sdl会按照一定的频率读取队列数据并写入带音频设备。
SDL_QueueAudio(audioId, data, dataSize);
使用完成后需要销毁资源,如下所示,SDL_Quit并不是必要的,通常是程序退出才需要调用,这个时候调不调已经无所谓了。
if (audioId >= 2)
SDL_CloseAudioDevice(audioId);
SDL_Quit();
使用push(SDL_QueueAudio)的方式播放音频,通常会遇到一个问题:应该以什么频率往队列写入多少数据?如何保持队列长度稳定,且不会因为数据过少导致声音卡顿。通用以定量的方式是不可行的,基本都会出现数据量少卡顿或队列长度不断增长。这时候我们需要能够动态的控制队列长度,数据少了就写入快一些,数据过多就写入慢一些。
写入过快或者慢都会出现问题。
写入过快时队列长度不受控制的增长,如果播放时间足够长就会导致out of memory。
写入过慢则会导致队列数据不足,sdl会自动补充静音包,呈现出来的结果就是播放的声音断断续续的。
比较简单的动态控制算法就是pid了,我们只需要根据当前队列的长度计算出需要调整的延时,即能够控制队列长度:(示例)
//目标队列长度
double targetSize;
//当前队列长度
int size;
error_p = targetSize - size;
error_i += error_p;
error_d = error_p - error_dp;
error_dp = error_p;
size = (kp * error_p + ki * error_i + kd * error_d);
//将targetSize - size转换成时长就是延时。
double delay;
效果预览:
目标队列长度是49152bytes,基本在可控范围内波动。
将上述代码合并起来形成一个完整的音频解码播放流程:
示例的sdk版本:ffmpeg 4.3、sdl2
windows、linux都可以正常运行
#include
#include
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#undef main
int main(int argc, char** argv) {
const char* input = "test_music.wav";
enum AVSampleFormat forceFormat;
AVFormatContext* pFormatCtx = NULL;
AVCodecContext* pCodecCtx = NULL;
const AVCodec* pCodec = NULL;
AVDictionary* opts = NULL;
AVPacket packet;
AVFrame* pFrame = NULL;
struct SwrContext* swr_ctx = NULL;
uint8_t* outBuffer = NULL;
int audioindex = -1;
int exitFlag = 0;
int isLoop = 1;
SDL_AudioSpec wanted_spec, spec;
int audioId = 0;
memset(&packet, 0, sizeof(AVPacket));
//初始化SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
printf("Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}
//打开输入流
if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) {
printf("Couldn't open input stream.\n");
goto end;
}
//查找输入流信息
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
printf("Couldn't find stream information.\n");
goto end;
}
//获取音频流
for (unsigned i = 0; i < pFormatCtx->nb_streams; i++)
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
audioindex = i;
break;
}
if (audioindex == -1) {
printf("Didn't find a audio stream.\n");
goto end;
}
//创建解码上下文
pCodecCtx = avcodec_alloc_context3(NULL);
if (pCodecCtx == NULL)
{
printf("Could not allocate AVCodecContext\n");
goto end;
}
//获取解码器
if (avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[audioindex]->codecpar) < 0)
{
printf("Could not init AVCodecContext\n");
goto end;
}
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if (pCodec == NULL) {
printf("Codec not found.\n");
goto end;
}
//使用多线程解码
if (!av_dict_get(opts, "threads", NULL, 0))
av_dict_set(&opts, "threads", "auto", 0);
//打开解码器
if (avcodec_open2(pCodecCtx, pCodec, &opts) < 0) {
printf("Could not open codec.\n");
goto end;
}
if (pCodecCtx->sample_fmt == AV_SAMPLE_FMT_NONE)
{
printf("Unknown sample foramt.\n");
goto end;
}
if (pCodecCtx->sample_rate <= 0 || av_get_channel_layout_nb_channels(pFormatCtx->streams[audioindex]->codecpar->channels) <= 0)
{
printf("Invalid sample rate or channel count!\n");
goto end;
}
//打开设备
wanted_spec.channels = pFormatCtx->streams[audioindex]->codecpar->channels;
wanted_spec.freq = pCodecCtx->sample_rate;
wanted_spec.format = AUDIO_F32SYS;
wanted_spec.silence = 0;
wanted_spec.samples = FFMAX(512, 2 << av_log2(wanted_spec.freq / 30));
wanted_spec.callback = NULL;
wanted_spec.userdata = NULL;
audioId = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec, 1);
if (audioId < 2)
{
printf("Open audio device error!\n");
goto end;
}
switch (spec.format)
{
case AUDIO_S16SYS:
forceFormat = AV_SAMPLE_FMT_S16;
break;
case AUDIO_S32SYS:
forceFormat = AV_SAMPLE_FMT_S32;
break;
case AUDIO_F32SYS:
forceFormat = AV_SAMPLE_FMT_FLT;
break;
default:
printf("audio device format was not surported!\n");
goto end;
break;
}
pFrame = av_frame_alloc();
SDL_PauseAudioDevice(audioId, 0);
start:
while (!exitFlag)
{
//读取包
int gotPacket = av_read_frame(pFormatCtx, &packet) == 0;
if (!gotPacket || packet.stream_index == audioindex)
//!gotPacket:未获取到packet需要将解码器的缓存flush,所以还需要进一次解码流程。
{
//发送包
if (avcodec_send_packet(pCodecCtx, &packet) < 0)
{
printf("Decode error.\n");
av_packet_unref(&packet);
goto end;
}
//接收解码的帧
while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) {
uint8_t* data;
size_t dataSize;
if (forceFormat != pCodecCtx->sample_fmt || spec.freq != pFrame->sample_rate || spec.channels != pFrame->channels)
//重采样
{
//计算输入采样数
int out_count = (int64_t)pFrame->nb_samples * spec.freq / pFrame->sample_rate + 256;
//计算输出数据大小
int out_size = av_samples_get_buffer_size(NULL, spec.channels, out_count, forceFormat, 0);
//输入数据指针
const uint8_t** in = (const uint8_t**)pFrame->extended_data;
//输出缓冲区指针
uint8_t** out = &outBuffer;
int len2 = 0;
if (out_size < 0) {
av_log(NULL, AV_LOG_ERROR, "av_samples_get_buffer_size() failed\n");
goto end;
}
if (!swr_ctx)
//初始化重采样对象
{
swr_ctx = swr_alloc_set_opts(NULL, av_get_default_channel_layout(spec.channels), forceFormat, spec.freq, av_get_default_channel_layout(pFormatCtx->streams[audioindex]->codecpar->channels), pCodecCtx->sample_fmt, pCodecCtx->sample_rate, 0, NULL);
if (!swr_ctx || swr_init(swr_ctx) < 0) {
av_log(NULL, AV_LOG_ERROR, "swr_alloc_set_opts() failed\n");
goto end;
}
}
if (!outBuffer)
//申请输出缓冲区
{
outBuffer = (uint8_t*)av_mallocz(out_size);
}
//执行重采样
len2 = swr_convert(swr_ctx, out, out_count, in, pFrame->nb_samples);
if (len2 < 0) {
av_log(NULL, AV_LOG_ERROR, "swr_convert() failed\n");
goto end;
}
//取得输出数据
data = outBuffer;
//输出数据长度
dataSize = av_samples_get_buffer_size(0, spec.channels, len2, forceFormat, 1);
}
else
{
data = pFrame->data[0];
dataSize = av_samples_get_buffer_size(pFrame->linesize, pFrame->channels, pFrame->nb_samples, forceFormat, 0);
}
//写入数据
SDL_QueueAudio(audioId, data, dataSize);
//延时,按照数据长度,-1是防止写入过慢卡顿
SDL_Delay((dataSize) * 1000.0 / (spec.freq * av_get_bytes_per_sample(forceFormat) * spec.channels) - 1);
int size = SDL_GetQueuedAudioSize(audioId);
printf("queue size:%dbytes\n", size);
}
}
av_packet_unref(&packet);
if (!gotPacket)
{
//循环播放时flush出缓存帧后需要调用此方法才能重新解码。
avcodec_flush_buffers(pCodecCtx);
break;
}
}
if (!exitFlag)
{
if (isLoop)
{
//定位到起点
if (avformat_seek_file(pFormatCtx, -1, 0, 0, 0, AVSEEK_FLAG_FRAME) >= 0)
{
goto start;
}
}
}
end:
//销毁资源
if (pFrame)
{
if (pFrame->format != -1)
{
av_frame_unref(pFrame);
}
av_frame_free(&pFrame);
}
if (packet.data)
{
av_packet_unref(&packet);
}
if (pCodecCtx)
{
avcodec_close(pCodecCtx);
avcodec_free_context(&pCodecCtx);
}
if (pFormatCtx)
avformat_close_input(&pFormatCtx);
if (pFormatCtx)
avformat_free_context(pFormatCtx);
swr_free(&swr_ctx);
av_dict_free(&opts);
if (outBuffer)
av_free(outBuffer);
if (audioId >= 2)
SDL_CloseAudioDevice(audioId);
SDL_Quit();
return 0;
}
包含pid的完整代码,项目为vs2022,目录中包含makefile,windows、linux都可以正常运行。
下载链接
以上就是今天要讲的内容,总的来说,使用ffmpeg解码音频sdl播放流程是基本与视频一致的,而且使用push的方式,相对与pull的方式,不需要使用额外的队列以及条件变量做访问控制。但是音频队列数据长度的控制也是一个难点,虽然本文使用pid达到了目的,但长度还是存在动态波动,需要继续调参或者调整计算逻辑或者采取其他方式去优化。