> 技术文档 > Linux驱动21 --- FFMPEG 音频 API

Linux驱动21 --- FFMPEG 音频 API


 目录

 一、FFMPEG 音频 API

1.1 解码步骤

        创建核心上下文指针

        打开输入流

        获取输入流

        获取解码器

        初始化解码器

        创建输入流指针

        创建输出流指针

        初始化 SDL

        配置音频参数

        打开音频设备

        获取一帧数据

        发送给解码器

        从解码器获取数据

        开辟数据空间

        初始化内存        

        音频重采样配置 --- 相当于视频的格式转换

        由通道数获取默认的通道布局

        初始化重采样核心结构体

        音频重采样

        播放

        延时

1.2 参数扩展

        SDL_AudioSpec

        AVFrame

二、FFMPEG 录制声音的过程

2.1 步骤

        FFMPEG 注册所有  

        FFMPEG 核心上下文申请

        查找音频设备

        注册音频设备

        SDL 初始化

        配置音频参数

        打开音频设备

        读取一帧数据

        写入到文件

三、如何在板子上实现


一、FFMPEG 音频 API

1.1 解码步骤

        创建核心上下文指针

                AVFormatContext * avfmtctx  = avformat_alloc_context();

        打开输入流

                avformat_open_input(&avfmtctx, argv[1], NULL, NULL);

        获取输入流

                avformat_find_stream_info(avfmtctx, NULL);

        获取解码器

                AVCodecContext * avcodectx = avfmtctx->streams[0]->codec;
                AVCodec *avcodec = avcodec_find_decoder(avcodectx->codec_id);

        初始化解码器

                avcodec_open2(avcodectx, avcodec, NULL); 

        创建输入流指针

                AVPacket * avpkt = av_packet_alloc();

        创建输出流指针

                AVFrame * avfrm = av_frame_alloc(); 

        初始化 SDL

        函数头文件

                #include

        函数原型

                int SDL_Init(Uint32 flags)

        函数参数

                常用参数

                        SDL_INIT_TIMER

                        SDL_INIT_AUDIO

                        SDL_INIT_VIDEO

        函数返回值

                成功时返回0,失败时返回负数错误码; 调用SDL_GetError()可以获得本次异常信息。

        配置音频参数

        函数原型

                int SDL_OpenAudioDevice(const char  *device,int iscapture,const SDL_AudioSpec *desired, SDL_AudioSpec *obtained,int allowed_changes);  

        函数参数

                device:音频设备的名称,NULL表示使用默认设备

                iscapture:设为0,非0的值在当前SDL2版本还不支持

                desired:期望得到的音频输出格式

                obtained:实际的输出格式

                allowed_changes:该参数用来指定 当期望和实际的不一样时,能不能够对某一些输出参数进行修改。 设为0,则不能修改。设为如下的值,则可对相应的参数修改:

                        SDL_AUDIO_ALLOW_FREQUENCY_CHANGE

                        SDL_AUDIO_ALLOW_FORMAT_CHANGE

                        SDL_AUDIO_ALLOW_CHANNELS_CHANGE

                        SDL_AUDIO_ALLOW_ANY_CHANGE

        函数返回值

                0失败;成功返回有效音频设备号 >= 2

        打开音频设备

        函数原型

                void SDLCALL SDL_PauseAudioDevice(SDL_AudioDeviceID dev, int pause_on)

        函数参数

                dev:SDL_OpenAudioDevice函数返回值

                pause_on:根据介绍,填0即可

        av_read_frame

        avcodec_send_packet

        avcodec_receive_frame

        获取一帧数据

                av_read_frame(avfmtctx, avpkt)

        发送给解码器

                avcodec_send_packet(avcodectx, avpkt);

        从解码器获取数据

                avcodec_receive_frame(avcodectx, avfrm);

        开辟数据空间

        函数原型

                int av_samples_get_buffer_size(int *linesize, int nb_channels, int nb_samples, enum AVSampleFormat sample_fmt, int align)

        函数参数

                linesize:计算的lineize,可能为NULL

                nb_channels:声道数

                SDL_AudioSpec结构体中channels成员变量

                nb_samples:单个通道中的样本数

                        采样频率(Hz) *当前帧的音频采样数/当前帧的音频数据的采样率

                sample_fmt:样本格式

                align:对齐缓冲区大小对齐(0 =默认,1 =无对齐)

        函数返回值

                需要的缓冲区大小,或失败时出现负错误代码

        初始化内存        

                调用av_malloc,然后再将内存内容清零

        函数原型

                void *av_mallocz(size_t size)

                申请数据存放空间

        音频重采样配置 --- 相当于视频的格式转换

        根据输入和输出参数,并设置相关选项

        函数头文件

                #include \"libswresample/swresample.h\"

        函数原型

        SwrContext *swr_alloc_set_opts(SwrContext *s, int64_t out_ch_layout, enum AVSampleFormat out_sample_fmt, int out_sample_rate, int64_t in_ch_layout, enum AVSampleFormat in_sample_fmt, int in_sample_rate, int log_offset, void *log_ctx)

        函数参数

s:可选的现有SwrContext,如果不为NULL,则会使用该现有上下文。out_ch_layout:输出声道布局(Channel Layout) 通过函数av_get_default_channel_layout获取 根据SDL_AudioSpec的channels获取out_sample_fmt:输出采样格式 根据SDL_AudioSpec的format成员选取out_sample_rate:输出采样率 SDL_AudioSpec的freq成员in_ch_layout:输入声道布局 通过函数av_get_default_channel_layout获取 输入流codecpar下的channelsin_sample_fmt:输入采样格式 输入流codec下的sample_fmtin_sample_rate:输入采样率 输入流codec下的sample_ratelog_offset:日志偏移量,填0即可log_ctx:日志上下文,填NULL即可
        由通道数获取默认的通道布局

        函数原型

                int64_t av_get_default_channel_layout(int nb_channels);    

        初始化重采样核心结构体

        函数原型

                int swr_init(struct SwrContext *s);

        函数参数

                s:swr_alloc_set_opts返回值

        音频重采样

                针对每一帧音频的处理。把一帧帧的音频作相应的重采样

        函数原型

                int swr_convert(struct SwrContext *s, uint8_t **out, int out_count, const uint8_t **in, int in_count);

s:音频重采样的上下文out:输出的指针。传递的输出的数组out_count:输出的样本数量,不是字节数。单通道的样本数量。 av_samples_get_buffer_size参数nb_samplesin:输入的数组,AVFrame解码出来的DATA(成员extended_data) in_count:输入的单通道的样本数量 AVFrame结构体的nb_samples成员

        函数返回值

                每个通道输出的样本数,失败为负值

        播放

        函数功能

                使用此函数可以在回调设备(即使用了第一套API需要回调函数填充数据的设备)上缓存更多音频,而不必通过回调函数填充音频数据。

        函数原型

                int SDL_QueueAudio(SDL_AudioDeviceID dev, const void* data, Uint32 len)

        函数参数

                dev:设备ID

                data:需要被填充的数据指针

                len:数据buffer长度,byte为单位

                        通过函数av_samples_get_buffer_size获取

        函数返回值

                0表示成功,非零表示出现异常

        //av_samples_get_buffer_size

        延时

        SDL_Delay

        SDL_Delay((输出数据大小) * 1000.0 / (音频采样率 * av_get_bytes_per_sample(音频格式) * 通道数量) - 1);

1.2 参数扩展

        SDL_AudioSpec

int freq;freq 每秒钟发送给音频设备的sample frame的个数,通常是11025,220502,44100和48000。(sample frame = 样本精度 * 通道数)//输入流codec中sample_rate成员SDL_AudioFormat format;fromat 每个样本占用的空间大小及格式,例如 AUDIO_S16SYS,样本是有符号的16位整数,字节顺序(大端还是小端)和系统一样。更多的格式可参考SDL_AudioFormat。// AUDIO_F32SYSUint8 channels; channels 通道数,在SDL2.0中支持1(mono),2(stereo),4(quad)和6(5.1)//输入流codecpar中channels成员Uint8 silence;silence 音频数据中表示静音的值是多少//填0即可Uint16 samples;这是每次读取的采样数量,‌决定了音频数据回调的频率。‌例如,‌设置为1024时,‌表示每次读取1024个样本数据,‌回调函数被调用一次。‌这个值不一定是2的幂指数次方,‌最好由AVFrame->nb_samples参数赋值。‌// 512 Uint16 padding;对于某些环境需要 Uint32 size;size 缓冲区的大小(字节为单位),当我们想要更多声音的时候,我们想让SDL给出来的声音缓冲区的尺寸。一个比较合适的值在512到8192之间;ffplay使用1024 SDL_AudioCallback callback; /**< Callback that feeds the audio device (NULL to use SDL_QueueAudio()). */callback用来音频设备缓冲区的回调函数void *userdata;userdata在回调函数中使用的数据指针

        AVFrame

typedef struct AVFrame {#define AV_NUM_DATA_POINTERS 8 uint8_t *data[AV_NUM_DATA_POINTERS]; // 存放媒体数据的指针数组 int linesize[AV_NUM_DATA_POINTERS]; // 视频或音频帧数据的行宽 uint8_t **extended_data; // 音频或视频数据的指针数组。 int width, height; // 视频帧的款和高 /** * number of audio samples (per channel) described by this frame */ int nb_samples; // 当前帧的音频采样数(每个通道) int format; // 视频帧的像素格式,见enum AVPixelFormat,或音频的采样格式,见enum AVSampleForma int key_frame; // 当前帧是否为关键帧,1表示是,0表示不是。 AVRational sample_aspect_ratio; // 视频帧的样本宽高比 int64_t pts; // 以time_base为单位的呈现时间戳(应向用户显示帧的时间)。 int64_t pkt_dts; // 从AVPacket复制而来的dts时间,当没有pts时间是,pkt_dts可以替代pts。 int coded_picture_number; // 按解码先后排序的,解码图像数 int display_picture_number; // 按显示前后排序的,显示图像数。 int quality; // 帧质量,从1~FF_LAMBDA_MAX之间取之,1表示最好,FF_LAMBDA_MAX之间取之表示最坏。 void *opaque; // user的私有数据。 int interlaced_frame; // 图片的内容是隔行扫描的(交错帧)。 int top_field_first; // 如果内容是隔行扫描的,则首先显示顶部字段。 int sample_rate; // 音频数据的采样率 uint64_t channel_layout; // 音频数据的通道布局。 /** * AVBuffer引用,当前帧数据。 如果所有的元素为NULL,则此帧不是引用计数。 必须连续填充此数组, * 即如果buf [i]为非NULL,j <i,buf[j]也必须为非NULL。 * * 每个数据平面最多可以有一个AVBuffer,因此对于视频,此数组始终包含所有引用。 * 对于具有多于AV_NUM_DATA_POINTERS个通道的平面音频,可能有多个缓冲区可以容纳在此阵列中。 * 然后额外的AVBufferRef指针存储在extended_buf数组中。 */ AVBufferRef *buf[AV_NUM_DATA_POINTERS]; AVBufferRef **extended_buf; // AVBufferRef的指针 int nb_extended_buf; // extended_buf的数量 enum AVColorSpace colorspace; // YUV颜色空间类型。 int64_t best_effort_timestamp; // 算法预测的timestamp int64_t pkt_pos; // 记录上一个AVPacket输入解码器的位置。 int64_t pkt_duration; // packet的duration AVDictionary *metadata; int channels; // 音频的通道数。 int pkt_size; // 包含压缩帧的相应数据包的大小。} AVFrame;

二、FFMPEG 录制声音的过程

2.1 步骤

        FFMPEG 注册所有  

        头文件

                #include \"libavdevice/avdevice.h\"
        函数原型

                void avdevice_register_all(void)

        FFMPEG 核心上下文申请

                AVFormatContext * avfmtctx  = avformat_alloc_context();

        查找音频设备

        函数原型

                AVInputFormat *av_find_input_format(const char *short_name)

        函数参数

                直接填alsa即可

        函数返回值

                就是需要的输入设备的核心上下文指针

        注册音频设备

        函数原型

                int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options)

        函数参数

                ps:FFMPEG的核心上下文指针

                url:在此需要使用声卡的名字

                \"plughw:CARD=AudioPCI,DEV=0\"

                fmt:av_find_input_format函数返回值

                options:填NULL

        SDL 初始化

                SDL_Init(SDL_INIT_AUDIO);

        配置音频参数

        函数原型

                int SDL_OpenAudio(SDL_AudioSpec *desired, SDL_AudioSpec *obtained)

                        desired:想要的配置

                        obtained:实际得到的配置,此处填NULL即可

        打开音频设备

                void SDL_PauseAudio(int pause_on)

        读取一帧数据

                int av_read_frame(AVFormatContext *s, AVPacket *pkt)

        写入到文件

                fwrite

三、如何在板子上实现

1、在 buildroot 中勾选 ffmpeg 选项、SDL 选项

2、编译文件系统 --- 生成新的文件系统

3、烧录新的文件系统

4、和之前 LVGL 相同 --- 修改 Makefile

        4.1 CC 换成 buildroot 的交叉编译工具

        4.2 把之前该删除的依赖库删除,把需要的库给加上

5、编译

        可能出现的问题

        buildroot 支持的 ffmpeg 和 SDL 版本和程序中使用的版本不符

6、运行

代码

dec_audio.c //音频

#include #include \"libavformat/avformat.h\"#include \"libswresample/swresample.h\"#include int main(int argc, char *argv[]){ if(argc < 2) { printf(\"./play \\n\"); return 0; } //创建核心上下文指针 AVFormatContext * avfmtctx = avformat_alloc_context(); //打开输入流 avformat_open_input(&avfmtctx, argv[1], NULL, NULL); //获取输入流 avformat_find_stream_info(avfmtctx, NULL); //到此会获取 //输入流获取之后获取的是纯音频文件,只有一个流 ,所以还用 avfmt->streams[0] //有的音频,会带一个图片封面 --- 这种音频会报段错误 //获取解码器 AVCodecContext * avcodectx = avfmtctx->streams[0]->codec; AVCodec *avcodec = avcodec_find_decoder(avcodectx->codec_id); //初始化解码器 avcodec_open2(avcodectx, avcodec, NULL); //创建输入流指针 AVPacket * avpkt = av_packet_alloc(); //存放输入流中一帧图像 //创建输出流指针 AVFrame * avfrm = av_frame_alloc(); //初始化SDL SDL_Init(SDL_INIT_AUDIO); //配置音频参数 SDL_AudioSpec desired, obtained; //一个期望的,一个获得的 desired.callback = NULL; desired.channels = 2; //期望的通道数 desired.format = AUDIO_S16SYS; //音频的格式 desired.freq = avcodectx->sample_rate; //采样率 --- 需要注意 desired.padding = 0; desired.samples = 1152; //采样数 desired.silence = 0; desired.size = 0; // desired.userdata = NULL; //打开音频设备 SDL_AudioDeviceID aid = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, SDL_AUDIO_ALLOW_ANY_CHANGE); //理论上ID会大于0 //启动音频设备 SDLCALL SDL_PauseAudioDevice(aid, 0); enum AVSampleFormat mysfmt; switch(obtained.format) { case AUDIO_S16SYS: mysfmt = AV_SAMPLE_FMT_S16; break; case AUDIO_S32SYS: mysfmt = AV_SAMPLE_FMT_S32; break; case AUDIO_F32SYS: mysfmt = AV_SAMPLE_FMT_FLT; break; } int size; uint8_t *data = NULL; while(1) { //获取一帧数据 if(av_read_frame(avfmtctx, avpkt) != 0) { printf(\"获取文件错误/到达文件结尾\\n\"); break; } //发送给解码器 avcodec_send_packet(avcodectx, avpkt); //从解码器获取数据 avcodec_receive_frame(avcodectx, avfrm); //第一帧和第二帧获取的大小不一样 --- 把开辟空间方在里面 //开辟数据空间 size = av_samples_get_buffer_size(NULL, obtained.channels, avfrm->nb_samples, mysfmt, 0); //初始化内存 data = av_mallocz(size); //音频重采样配置 --- 相当于视频的格式转换 struct SwrContext * swrctx = swr_alloc_set_opts(NULL, av_get_default_channel_layout(obtained.channels), mysfmt, obtained.freq, \\ av_get_default_channel_layout(avcodectx->channels), avcodectx->sample_fmt, avcodectx->sample_rate, 0, NULL); //初始化重采样核心结构体 swr_init(swrctx); //音频重采样 swr_convert(swrctx, &data, size, avfrm->extended_data, avfrm->nb_samples); //播放 SDL_QueueAudio(aid, data, size); //延时 //SDL_Delay((输出数据大小) * 1000.0 / (音频采样率 * av_get_bytes_per_sample(音频格式) * 通道数量) - 1); SDL_Delay((size) * 1000.0 / (obtained.freq * av_get_bytes_per_sample(mysfmt) * obtained.channels) - 1); //网上有两种说法,1.当前的音频播放需要时间 2.音频帧不够,通过延时,补全 av_free(data); }}

get_audio.c //录音

#include #include \"libavformat/avformat.h\"#include #include #include #include \"libavdevice/avdevice.h\"int end_flag = 0;void *pthread_count_func(void *arg){ int num = 0; while(num--) { sleep(1); printf(\"录音剩余 %d 秒\\n\", num); } end_flag = 1;}int main(void){ //FFMPEG注册所有 --- 必须写 avdevice_register_all(); //FFMPEG核心上下文申请 AVFormatContext * avfmtctx = avformat_alloc_context(); //查找音频设备 AVInputFormat * avifmt = av_find_input_format(\"alsa\"); //注册音频设备 avformat_open_input(&avfmtctx, \"hw:CARD=AudioPCI,DEV=0\", avifmt, NULL); //SDL初始化 SDL_Init(SDL_INIT_AUDIO); //配置音频参数 --- 再此配置为期望的,得到的填NULL即可 SDL_AudioSpec desired; //一个期望的,一个获得的 desired.callback = NULL; desired.channels = 2; //期望的通道数 desired.format = AUDIO_S16SYS; //音频的格式 desired.freq = 48000; //采样率 --- 过低声音会很奇怪 desired.padding = 0; desired.samples = 1152; //采样数 desired.silence = 0; desired.size = 0; // desired.userdata = NULL; SDL_OpenAudio(&desired, NULL); //打开音频设备 SDL_PauseAudio(0); AVPacket * avpkt = av_packet_alloc(); //存放输入流中一帧数据 FILE *file = fopen(\"./9203.pcm\", \"w\"); pthread_t pd = 0; pthread_create(&pd, NULL, pthread_count_func, NULL); while(1) { if(end_flag) { break; } //读取一帧数据 av_read_frame(avfmtctx, avpkt); //写入到文件 fwrite(avpkt->data, 1, avpkt->size, file); } fclose(file); return 0;}