前言
ffmpeg解码音频之后进行播放,本篇使用SDL播放ffmpeg解码音频流转码后的pcm。
FFmpeg解码音频
FFmpeg解码音频的基本流程请参照:《FFmpeg开发笔记(七):ffmpeg解码音频保存为PCM并使用软件播放》
SDL播放音频
SDL播放音频的基本流程请参照:《SDL开发笔记(二):音频基础介绍、使用SDL播放音频》
ffmpeg音频同步
ffmpeg同步包含音频、视频、字幕等等,此处描述的同步是音频的同步。
基本流程
同步关键点
不改变播放速度的前提下,音频的播放相对容易,本文章暂时未涉及到音视频双轨或多轨同步。
解码音频后,时间间隔还是计算一下,主要是控制解码的间隔,避免解码过快导致缓存区溢出导致异常。
解码音频进行重采样之后可以得到指定采样率、声道、数据类型的固定参数,使用SDL用固定参数打开音频,将解码的数据扔向缓存区即可。因为解码的时候其数据量与采样率是对应的,播放的时候也是扔入对应的数据量,所以再不改变音频采样率的前提下,我们是可以偷懒不做音频同步的。
压入数据缓存区,可以根据播放的回调函数之后数据缓存区的大小进行同步解码压入音频,但是音频与视频不同,音频卡顿的话对音频播放的效果将会大打折扣,导致音频根本无法被顺利的播放,非常影响用户体验,实测需要保留一倍以上的预加载的音频缓冲区,否则等需要的是再加载就已经晚了。
音频更为复杂的操作涉及到倍速播放、音调改变等等,后续文章会有相应的文章说明。
ffmpeg音频同步相关结构体详解
AVCodecContext
该结构体是编码上下文信息,对文件流进行探测后,就能得到文件流的具体相关信息了,关于编解码的相关信息就在此文件结构中。
与同步视频显示相关的变量在此详解一下,其他的可以自行去看ffmpeg源码上对于结构体AVCodecContext的定义。
struct AVCodecContext { AVMediaType codec_type; // 编码器的类型,如视频、音频、字幕等等 AVCodec *codec; // 使用的编码器 enum AVSampleFormat sample_fmt; // 音频采样点的数据格式 int sample_rate; // 每秒的采样率 int channels; // 音频通道数据量 uint64_t channel_layout; // 通道布局 } AVCodecContext;
SwrContext
重采样的结构体,最关键的是几个参数,输入的采样频率、通道布局、数据格式,输出的采样频率、通道布局、数据格式。
此结构是不透明的。这意味着如果要设置选项,必须使用API,无法直接将属性当作结构体变量进行设置。
- 属性“out_ch_layout”:输入的通道布局,需要通过通道转换函数转换成通道布局枚举,其通道数与通道布局枚举的值是不同的,
- 属性“out_sample_fmt”:输入的采样点数据格式,解码流的数据格式即可。
- 属性“out_sample_rate”:输入的采样频率,解码流的采样频率。
- 属性“in_ch_layout”:输出的通道布局。
- 属性“in_sample_fmt”:输出的采样点数据格式。
- 属性“in_sample_rate”:输出的采样频率。
Demo源码
void FFmpegManager::testDecodeAudioPlay() { QString fileName = "E:/testFile2/1.mp3"; // 输入解码的文件 QFile file("D:/1.pcm"); AVFormatContext *pAVFormatContext = 0; // ffmpeg的全局上下文,所有ffmpeg操作都需要 AVStream *pAVStream = 0; // ffmpeg流信息 AVCodecContext *pAVCodecContext = 0; // ffmpeg编码上下文 AVCodec *pAVCodec = 0; // ffmpeg编码器 AVPacket *pAVPacket = 0; // ffmpag单帧数据包 AVFrame *pAVFrame = 0; // ffmpeg单帧缓存 SwrContext *pSwrContext = 0; // ffmpeg音频转码 SDL_AudioSpec sdlAudioSpec; // sdk音频结构体,用于打开音频播放器 int ret = 0; // 函数执行结果 int audioIndex = -1; // 音频流所在的序号 int numBytes = 0; // 音频采样点字节数 uint8_t * outData[8] = {0}; // 音频缓存区(不带P的) int dstNbSamples = 0; // 解码目标的采样率 int outChannel = 0; // 重采样后输出的通道 AVSampleFormat outFormat = AV_SAMPLE_FMT_NONE; // 重采样后输出的格式 int outSampleRate = 0; // 重采样后输出的采样率 pAVFormatContext = avformat_alloc_context(); // 分配 pAVPacket = av_packet_alloc(); // 分配 pAVFrame = av_frame_alloc(); // 分配 if(!pAVFormatContext || !pAVPacket || !pAVFrame) { LOG << "Failed to alloc"; goto END; } // 步骤一:注册所有容器和编解码器(也可以只注册一类,如注册容器、注册编码器等) av_register_all(); // 步骤二:打开文件(ffmpeg成功则返回0) LOG << "文件:" << fileName << ",是否存在:" << QFile::exists(fileName); ret = avformat_open_input(&pAVFormatContext, fileName.toUtf8().data(), 0, 0); if(ret) { LOG << "Failed"; goto END; } // 步骤三:探测流媒体信息 ret = avformat_find_stream_info(pAVFormatContext, 0); if(ret < 0) { LOG << "Failed to avformat_find_stream_info(pAVCodecContext, 0)"; goto END; } // 步骤四:提取流信息,提取视频信息 for(int index = 0; index < pAVFormatContext->nb_streams; index++) { pAVCodecContext = pAVFormatContext->streams[index]->codec; pAVStream = pAVFormatContext->streams[index]; switch (pAVCodecContext->codec_type) { case AVMEDIA_TYPE_UNKNOWN: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_UNKNOWN"; break; case AVMEDIA_TYPE_VIDEO: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_VIDEO"; break; case AVMEDIA_TYPE_AUDIO: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_AUDIO"; audioIndex = index; break; case AVMEDIA_TYPE_DATA: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_DATA"; break; case AVMEDIA_TYPE_SUBTITLE: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_SUBTITLE"; break; case AVMEDIA_TYPE_ATTACHMENT: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_ATTACHMENT"; break; case AVMEDIA_TYPE_NB: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_NB"; break; default: break; } // 已经找打视频品流 if(audioIndex != -1) { break; } } if(audioIndex == -1 || !pAVCodecContext) { LOG << "Failed to find video stream"; goto END; } // 步骤五:对找到的音频流寻解码器 pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id); if(!pAVCodec) { LOG << "Fialed to avcodec_find_decoder(pAVCodecContext->codec_id):" << pAVCodecContext->codec_id; goto END; } // 步骤六:打开解码器 ret = avcodec_open2(pAVCodecContext, pAVCodec, NULL); if(ret) { LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)"; goto END; } // 打印 LOG << "解码器名称:" <<pAVCodec->name << endl << "通道数:" << pAVCodecContext->channels << endl << "通道布局:" << av_get_default_channel_layout(pAVCodecContext->channels) << endl << "采样率:" << pAVCodecContext->sample_rate << endl << "采样格式:" << pAVCodecContext->sample_fmt; outChannel = 2; outSampleRate = 44100; outFormat = AV_SAMPLE_FMT_S16; // 步骤七:获取音频转码器并设置采样参数初始化 pSwrContext = swr_alloc_set_opts(0, // 输入为空,则会分配 av_get_default_channel_layout(outChannel), outFormat, // 输出的采样频率 outSampleRate, // 输出的格式 av_get_default_channel_layout(pAVCodecContext->channels), pAVCodecContext->sample_fmt, // 输入的格式 pAVCodecContext->sample_rate, // 输入的采样率 0, 0); ret = swr_init(pSwrContext); if(ret < 0) { LOG << "Failed to swr_init(pSwrContext);"; goto END; } // 最大缓存区,1152个采样样本,16字节,支持最长8个通道 outData[0] = (uint8_t *)av_malloc(1152 * 2 * 8); ret = SDL_Init(SDL_INIT_AUDIO); // SDL步骤一:初始化音频子系统 ret = SDL_Init(SDL_INIT_AUDIO); if(ret) { LOG << "Failed"; return; } // SDL步骤二:打开音频设备 sdlAudioSpec.freq = outSampleRate; sdlAudioSpec.format = AUDIO_S16LSB; sdlAudioSpec.channels = outChannel; sdlAudioSpec.silence = 0; sdlAudioSpec.samples = 1024; sdlAudioSpec.callback = callBack_fillAudioData; sdlAudioSpec.userdata = 0; ret = SDL_OpenAudio(&sdlAudioSpec, 0); if(ret) { LOG << "Failed"; return; } SDL_PauseAudio(0); _audioBuffer = (uint8_t *)malloc(102400); file.open(QIODevice::WriteOnly | QIODevice::Truncate); // 步骤八:读取一帧数据的数据包 while(av_read_frame(pAVFormatContext, pAVPacket) >= 0) { if(pAVPacket->stream_index == audioIndex) { // 步骤九:将封装包发往解码器 ret = avcodec_send_packet(pAVCodecContext, pAVPacket); if(ret) { LOG << "Failed to avcodec_send_packet(pAVCodecContext, pAVPacket) ,ret =" << ret; break; } // 步骤十:从解码器循环拿取数据帧 while(!avcodec_receive_frame(pAVCodecContext, pAVFrame)) { // nb_samples并不是每个包都相同,遇见过第一个包为47,第二个包开始为1152的 // LOG << pAVFrame->nb_samples; // 步骤十一:获取每个采样点的字节大小 numBytes = av_get_bytes_per_sample(outFormat); // 步骤十二:修改采样率参数后,需要重新获取采样点的样本个数 dstNbSamples = av_rescale_rnd(pAVFrame->nb_samples, outSampleRate, pAVCodecContext->sample_rate, AV_ROUND_ZERO); // 步骤十三:重采样 swr_convert(pSwrContext, outData, dstNbSamples, (const uint8_t **)pAVFrame->data, pAVFrame->nb_samples); // 第一次显示 static bool show = true; if(show) { LOG << numBytes << pAVFrame->nb_samples << "to" << dstNbSamples; show = false; } // 缓存区大小,小于一次回调获取的4097就得提前添加,否则声音会开盾 while(_audioLen > 4096 * 1) // while(_audioLen > 4096 * 0) { SDL_Delay(1); } _mutex.lock(); memcpy(_audioBuffer + _audioLen, outData[0], numBytes * dstNbSamples * outChannel); file.write((const char *)outData[0], numBytes * dstNbSamples * outChannel); _audioLen += numBytes * dstNbSamples * outChannel; _mutex.unlock(); } av_free_packet(pAVPacket); } } END: file.close(); LOG << "释放回收资源"; SDL_CloseAudio(); SDL_Quit(); if(outData[0]) { av_free(outData[0]); outData[0] = 0; LOG << "av_free(outData)"; } if(pSwrContext) { swr_free(&pSwrContext); pSwrContext = 0; } if(pAVFrame) { av_frame_free(&pAVFrame); pAVFrame = 0; LOG << "av_frame_free(pAVFrame)"; } if(pAVPacket) { av_free_packet(pAVPacket); pAVPacket = 0; LOG << "av_free_packet(pAVPacket)"; } if(pAVCodecContext) { avcodec_close(pAVCodecContext); pAVCodecContext = 0; LOG << "avcodec_close(pAVCodecContext);"; } if(pAVFormatContext) { avformat_close_input(&pAVFormatContext); avformat_free_context(pAVFormatContext); pAVFormatContext = 0; LOG << "avformat_free_context(pAVFormatContext)"; } } void FFmpegManager::callBack_fillAudioData(void *userdata, uint8_t *stream, int len) { SDL_memset(stream, 0, len); _mutex.lock(); if(_audioLen == 0) { _mutex.unlock(); return; } LOG << _audioLen << len; len = (len > _audioLen ? _audioLen : len); SDL_MixAudio(stream, _audioBuffer, len, SDL_MIX_MAXVOLUME); _audioLen -= len; memmove(_audioBuffer, _audioBuffer + len, _audioLen); _mutex.unlock(); // 每次加载4096 // LOG << _audioLen << len; }
工程模板v1.4.0
对应工程模板v1.4.0:增加解码音频转码使用SDL播放