ffmpeg 使用SDL2播放音频、视频 (2) 音频播放

学习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 = mCallbackSDL_OpenAudioSDL_PauseAudioSDL_AudioCallback
第二套callback = NULLSDL_OpenAudioDeviceSDL_PauseAudioDeviceSDL_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);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aworkholic

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值