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 <SDL2/SDL.h>
函数原型
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_F32SYS
Uint8 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参数赋值。// 512Uint16 padding; 对于某些环境需要Uint32 size; size 缓冲区的大小(字节为单位),当我们想要更多声音的时候,我们想让SDL给出来的声音缓冲区的尺寸。一个比较合适的值在512到8192之间;ffplay使用1024SDL_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 8uint8_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 AVSampleFormaint 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; // 算法预测的timestampint64_t pkt_pos; // 记录上一个AVPacket输入解码器的位置。int64_t pkt_duration; // packet的durationAVDictionary *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 <stdio.h>
#include "libavformat/avformat.h"
#include "libswresample/swresample.h"
#include <SDL2/SDL.h>int main(int argc, char *argv[])
{if(argc < 2){printf("./play <name>\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(); //初始化SDLSDL_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 <stdio.h>
#include "libavformat/avformat.h"
#include <SDL2/SDL.h>
#include <pthread.h>
#include <unistd.h>
#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;
}