学习ffmepg解码后的音视频通常只能保存为文件,或者进行再次编码保存或发送出去。为了在使用中直观中看到解码的结果,有必要使用SDL2进行展示。FFMPEG提供的工具ffplay可以播放解码后的音视频,实现使用了SDL库。
SDL(Simple DirectMedia Layer)是一套开放源代码的跨平台多媒体开发库,使用C语言写成,目前大版本已经到SDL2。SDL提供了数种控制图像、声音、输出入的函数,让开发者只要用相同或是相似的代码就可以开发出跨多个平台(Linux、Windows、Mac OS X等)的应用软件。其定位简化控制图像、声音、输出入等基础功能,更高级的绘图功能或是音效功能则需搭配OpenGL和OpenAL等API来达成。另外它本身也没有方便创建图形用户界面的函数。
SDL将功能分成下列数个子系统(subsystem):
Video(图像) — 图像控制以及线程(thread)和事件管理(event)。
Audio(声音) — 声音控制
Joystick(摇杆) — 游戏摇杆控制
CD-ROM(光盘驱动器) —光盘媒体控制
Window Management(视窗管理)— 与视窗程序设计集成
Event(事件驱动) —处理事件驱动
本文介绍SDL2最简单的音视频播放功能。
1、视频播放
参看博客 《ffmpeg 使用SDL2播放音频、视频 (1) 视频播放》
2、音频播放
SDL音频(PCM数据)播放的流程简单,主要有2个步骤
- (1). 初始化
(1.1) 初始化SDL
(1.2) 根据参数打开音频设备 - (2) 循环播放数据
(2.1) 播放音频数据
(2.2) 延时等待播放完成
关于SDL播放音频有两套API
SDL_AudioSpec | 打开音频设备 | 等待数据播放 | 数据填充 | |
---|---|---|---|---|
第一套 | callback = mCallback | SDL_OpenAudio | SDL_PauseAudio | SDL_AudioCallback |
第二套 | callback = NULL | SDL_OpenAudioDevice | SDL_PauseAudioDevice | SDL_QueueAudio |
这两套API不能混用,第一套需要自己实现回调函数,处理缓冲数据,兼容性好。第二套填充数据高效,不用考虑填充数据的时间。
2.1 回调方式播放音频
我们最直观的音频播放流程是,将播放的声音发送给声卡,这样扬声器就会将声音播放出来;只要我们不断的送数据,声音就会不停的输出。
接基本所有音频设备遵循以下原则,当声卡将要播放的声音输出到扬声器时,它首先会通过回调函数,向你要它一部分声频数据,然后拿着这部分音频数据去播放。等播放完了,它会再向你要下一部分。数据要多少,什么时候要,是声卡决定的,或者说是由底层api决定的。
以输入文件LoadingScreen_44.1k_s16le_stero.pcm
为例, SDL播放pcm数据,要求SDL_AudioSpec参数和数据缓冲区大小设置正确:
- 保证参数严格一致,freq = 44100、format = AUDIO_S16SYS、channels = 2;
- sample通常设置为2^n,如512、1024、2048等;
- 音频采样数据必须为packed格式
- pcm缓冲区的数据大小一定要是sample个采样数据字节的整数倍
例如,对于采样位为16,双声道,1024个sample时,pcm缓冲区大小必须是16/8 * 2 *1024 = 4096的整数倍,如4092,8192等。
按照开头的说明,SDL底层自动回调函数,读取需要的4096个字节。如果缓冲区大小设置4096,那么 主函数while中每次从pcm文件读取4096字节后,等待回调函数中读取缓冲区(回调每一次也读取4096字节),之后再从文件读取4096字节,等待回调执行。如果缓冲区大小设置8192,每次读取完pcm文件8192字节后,那么需要等到回调函数执行2次(4096*2)之后,才能进行下一次读取pcm文件。
#include <iostream>
#define SDL_MAIN_HANDLED
#include "SDL.h"
unsigned int audioLen = 0;
unsigned char *audioChunk = NULL;
unsigned char *audioPos = NULL;
void fill_audio(void * udata, Uint8 * stream, int len)
{
SDL_memset(stream, 0, len);
if(audioLen == 0)
return;
len = (len>audioLen ? audioLen : len);
SDL_MixAudio(stream, audioPos, len, SDL_MIX_MAXVOLUME);
audioPos += len;
audioLen -= len;
printf("buff use %d\n", len);
}
int main()
{
/// SDL
if(SDL_Init(SDL_INIT_AUDIO)) {
SDL_Log("init audio subsysytem failed.");
return 0;
}
SDL_AudioSpec wantSpec;
wantSpec.freq = 44100;
wantSpec.format = AUDIO_S16SYS;
wantSpec.channels = 2;
wantSpec.silence = 0;
wantSpec.samples = 1024; // 512, 1024 均可 采样数据缓冲区 基准
wantSpec.callback = fill_audio;
if(SDL_OpenAudio(&wantSpec, NULL) < 0) {
printf("can not open SDL!\n");
return -1;
}
FILE *fp = fopen("LoadingScreen_44.1k_s16le_stero.pcm", "rb+");
if(fp == NULL) {
printf("cannot open this file\n");
return -1;
}
// pcm数据缓冲区大小 = 采样数据缓冲区基准 的 整数倍
int pcm_buffer_size = wantSpec.samples * wantSpec.channels * SDL_AUDIO_BITSIZE(wantSpec.format) / 8;
char *pcm_buffer = (char *)malloc(pcm_buffer_size);
int data_count = 0;
// Play
SDL_PauseAudio(0);
while(1) {
if(fread(pcm_buffer, 1, pcm_buffer_size, fp) != pcm_buffer_size) {
break;
//SDL_Delay(3000);
Loop
//fseek(fp, 0, SEEK_SET);
//fread(pcm_buffer, 1, pcm_buffer_size, fp);
//data_count = 0;
}
data_count += pcm_buffer_size;
printf("Now Playing %10d Bytes data.\n", data_count);
//Set audio buffer (PCM data)
audioChunk = (Uint8 *)pcm_buffer;
//Audio buffer length
audioLen = pcm_buffer_size;
audioPos = audioChunk;
while(audioLen > 0) //等待声卡将缓冲区数据读取完毕
SDL_Delay(1);
}
free(pcm_buffer);
SDL_Quit();
}
2.2 非回调方式播放音频
代码比回调方式简单得多。
目前在windows上使用预编译的库,没有自己编译。在代码中数据推送都成功,但是没有声音。(后续再测试…); 后经测试,树莓派官方Debian系统可用。链接 树莓派 配置SDL2开发环境 音频播放测试。
#include <iostream>
#define SDL_MAIN_HANDLED
#include "SDL.h"
int main()
{
/// SDL
if(SDL_Init(SDL_INIT_AUDIO)) {
SDL_Log("init audio subsysytem failed.");
return 0;
}
SDL_AudioSpec wantSpec;
wantSpec.freq = 44100;
wantSpec.format = AUDIO_S16SYS; // 2字节
wantSpec.channels = 2; // 2个声道
wantSpec.silence = 0; //
wantSpec.samples = 1024; // 多取值2^n,如512,1024。 采样数据缓冲区 基准
wantSpec.callback = NULL;
SDL_AudioDeviceID deviceID;
if( (deviceID = SDL_OpenAudioDevice(NULL, 0, &wantSpec, NULL, SDL_AUDIO_ALLOW_ANY_CHANGE)) < 2) {
printf("can not open SDL!\n");
return -1;
}
FILE *fp = fopen("LoadingScreen_44.1k_s16le_stero.pcm", "rb+");
if(fp == NULL) {
printf("cannot open this file\n");
return -1;
}
int pcm_buffer_size = wantSpec.samples * wantSpec.channels * SDL_AUDIO_BITSIZE(wantSpec.format) / 8; // 采样数据缓冲区基准 的整数倍
char *pcm_buffer = (char *)malloc(pcm_buffer_size);
int data_count = 0;
// Play
SDL_PauseAudioDevice(deviceID,0);
while(1) {
if(fread(pcm_buffer, 1, pcm_buffer_size, fp) != pcm_buffer_size) {
break;
//SDL_Delay(3000);
Loop
//fseek(fp, 0, SEEK_SET);
//fread(pcm_buffer, 1, pcm_buffer_size, fp);
//data_count = 0;
}
data_count += pcm_buffer_size;
printf("Now Playing %10d Bytes data.\n", data_count);
SDL_QueueAudio(deviceID, pcm_buffer, pcm_buffer_size);
SDL_Delay(20); // 1024/44100 == 0.023 s
}
free(pcm_buffer);
SDL_Quit();
}
2.3 使用ffmpeg解码播放音频
不同于PCM音频数据,每一次读取后都存放于一块连续的内存中,ffmpge解码后的音频数据由于声道格式不同可能存放的数据不连续,另外,SDL支持的音频格式比ffmpeg要少,因此需要使用swr_convert对解码音频数据重新采样以符合SDL要求。
注意代码中 使用AVSampleFormat
构造SwrContext
代码、打开音频设备参数SDL_AudioSpec
代码。
#include <iostream>
#include <map>
#ifdef __cplusplus
extern "C" {
#endif
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include <libswresample/swresample.h>
#ifdef __cplusplus
}
#endif
#define SDL_MAIN_HANDLED
#include "SDL.h"
// audio sample rates map from FFMPEG to SDL (only non planar)
static std::map<int,int> AUDIO_FORMAT_MAP = {
// AV_SAMPLE_FMT_NONE = -1,
{AV_SAMPLE_FMT_U8, AUDIO_U8 },
{AV_SAMPLE_FMT_S16, AUDIO_S16SYS},
{AV_SAMPLE_FMT_S32, AUDIO_S32SYS},
{AV_SAMPLE_FMT_FLT, AUDIO_F32SYS}
};
#define MAX_AUDIO_FRAME_SIZE 192000 // 1 second of 48khz 32bit audio //48000 * (32/8)
unsigned int audioLen = 0;
unsigned char *audioChunk = NULL;
unsigned char *audioPos = NULL;
void fill_audio(void * udata, Uint8 * stream, int len)
{
SDL_memset(stream, 0, len);
if(audioLen == 0)
return;
len = (len>audioLen ? audioLen : len);
SDL_MixAudio(stream, audioPos, len, SDL_MIX_MAXVOLUME);
audioPos += len;
audioLen -= len;
}
int main()
{
const char* filename = "LoadingScreen.mp4";
// 1 输入流
AVFormatContext *input_fmt_ctx = NULL;
AVCodec *input_codec = NULL;
int ret;
if((ret = avformat_open_input(&input_fmt_ctx, filename, NULL, NULL)) < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");
return ret;
}
if((ret = avformat_find_stream_info(input_fmt_ctx, NULL)) < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");
return ret;
}
// 找到音频流(不需要遍历)
if((ret = av_find_best_stream(input_fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, &input_codec, 0)) < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot find an audio stream in the input file\n");
return ret;
}
int audio_stream_index = ret;
// 输出所有流信息
av_dump_format(input_fmt_ctx, audio_stream_index, NULL, 0);
//2 解码器
AVCodecContext *dec_ctx = avcodec_alloc_context3(input_codec);
if(!dec_ctx) {
av_log(NULL, AV_LOG_ERROR, "Could not allocate a decoding context\n");
avformat_close_input(&input_fmt_ctx);
return AVERROR(ENOMEM);
}
AVCodecParameters *codecpar = input_fmt_ctx->streams[audio_stream_index]->codecpar;
if((ret = avcodec_parameters_to_context(dec_ctx, codecpar)) < 0){
avformat_close_input(&input_fmt_ctx);
avcodec_free_context(&dec_ctx);
return ret;
}
if((ret = avcodec_open2(dec_ctx, input_codec, NULL)) < 0) {
avcodec_free_context(&dec_ctx);
avformat_close_input(&input_fmt_ctx);
return ret;
}
// 重采样contex
enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16; //声音格式 SDL仅支持部分音频格式
int out_sample_rate = /*48000; */ dec_ctx->sample_rate; //采样率
int out_channels = /*1; */ dec_ctx->channels; //通道数
int out_nb_samples = /*1024; */ dec_ctx->frame_size;
int out_buffer_size = av_samples_get_buffer_size(NULL, out_channels, out_nb_samples, out_sample_fmt, 1); //输出buff
unsigned char *outBuff = (unsigned char *)av_malloc(MAX_AUDIO_FRAME_SIZE * out_channels);
uint64_t out_chn_layout = av_get_default_channel_layout(dec_ctx->channels); //AV_CH_LAYOUT_STEREO; //通道布局 输出双声道
SwrContext *au_convert_ctx = swr_alloc_set_opts(NULL,
out_chn_layout, /*out*/
out_sample_fmt, /*out*/
out_sample_rate, /*out*/
dec_ctx->channel_layout, /*in*/
dec_ctx->sample_fmt, /*in*/
dec_ctx->sample_rate, /*in*/
0,
NULL);
swr_init(au_convert_ctx);
/// SDL
if(SDL_Init(SDL_INIT_AUDIO)) {
SDL_Log("init audio subsysytem failed.");
return 0;
}
SDL_AudioSpec wantSpec;
wantSpec.freq = out_sample_rate;
// 和SwrContext的音频重采样参数保持一致
wantSpec.format = AUDIO_FORMAT_MAP[out_sample_fmt];
wantSpec.channels = out_channels;
wantSpec.silence = 0;
wantSpec.samples = out_nb_samples;
wantSpec.callback = fill_audio;
wantSpec.userdata = dec_ctx;
if(SDL_OpenAudio(&wantSpec, NULL) < 0) {
printf("can not open SDL!\n");
return -1;
}
SDL_PauseAudio(0);
//3 解码
AVPacket packet;
AVFrame *frame = av_frame_alloc();
while(1)
{
if((ret = av_read_frame(input_fmt_ctx, &packet)) < 0)
break;
if(packet.stream_index == audio_stream_index)
{
ret = avcodec_send_packet(dec_ctx, &packet); // 送一帧到解码器
while(ret >= 0)
{
ret = avcodec_receive_frame(dec_ctx, frame); // 从解码器取得解码后的数据
if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
}
else if(ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error while sending a packet to the decoder\n");
goto end;
}
//static FILE *p = fopen("xxx.pcm", "wb");
//fwrite(p, frame->buf)
ret = swr_convert(au_convert_ctx, &outBuff, MAX_AUDIO_FRAME_SIZE, (const uint8_t **)frame->data, frame->nb_samples);
if(ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error while converting\n");
goto end;
}
//static FILE *outFile = fopen("xxx.pcm", "wb");
//fwrite(outBuff, 1, out_buffer_size, outFile);
while(audioLen > 0)
SDL_Delay(1);
audioChunk = (unsigned char *)outBuff;
audioPos = audioChunk;
audioLen = out_buffer_size;
av_frame_unref(frame);
}
}
av_packet_unref(&packet);
}
// 音频不需要 flush ?
end:
SDL_Quit();
avcodec_free_context(&dec_ctx);
avformat_close_input(&input_fmt_ctx);
av_frame_free(&frame);
swr_free(&au_convert_ctx);
}