0%

FFmpeg实战 音频录制、编码、重采样

命令行方式采集:

1
ffmpeg -f avfoundation -i :0 output/out.wav

准备

这里创建的是Mac App,以及引入的是动态库。由于动态库存放在一个共享位置,编译时就固定了它所在的位置,所以不需要拷贝到目录中。

  1. 引入并链接动态库文件(General/Frameworks, Libraries, and Embedded Content))。
  2. 添加头文件搜索目录(Build Settings/User Header Search Paths)。
  3. 创建C语言头文件以及实现文件,并创建Bridging-Header。

采集音频

打开设备

步骤:

  1. 注册设备。
  2. 设置采集方式(avfouncdation✔️/dshow/alsa)。
  3. 打开音频设备。

打开之后就可以录制音频流。

必要头文件:

1
2
3
#include "libavutil/avutil.h"
#include "libavdevice/avdevice.h"
#include "libavformat/avformat.h"

记得要引入的是动态库啊,静态库会有一堆符号找不到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 小于零则出错
int ret = 0;
AVFormatContext *context = NULL;
// <video device>:<audio device>
char *deviceName = ":0";
AVDictionary *options = NULL;
size_t errorBufferLength = 1024;
char errorBuffer[errorBufferLength];

// 1 注册设备
avdevice_register_all();

// 2 获取格式
AVInputFormat *inputFormat = av_find_input_format("avfoundation");

// 3 打开设备。这会同时创建上下文。
ret = avformat_open_input(&context, deviceName, inputFormat, &options);
if (ret < 0) {
// 输出到错误
av_strerror(ret, errorBuffer, errorBufferLength);
fprintf(stderr, "Failed to open audio device, [%d]%s\n", ret, errorBuffer);
return;
}

注意要先获取麦克风权限。

上下文创建后,记得要对应进行释放。

读取音频数据

av_read_frame:该方法既可以读取音频数据,也可以读取视频数据。

AVFormatContext:格式上下文,上面打开设备也用到。上下文是后续处理的基础,在打开设备的时候就可以获取上下文。

AVPacket,音视频数据包结构体。

返回0则表示成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用栈空间分配AVPacket
AVPacket pkt;
av_init_packet(&pkt);

int count = 0;
int ret = 0;
while ((ret == 0 || ret == -35) && count < 5) {
ret = av_read_frame(context, &pkt);

if (ret != 0) {
av_strerror(ret, error_buffer, kErrorLength);
fprintf(stderr, "Failed to reading, [%d]%s\n", ret, error_buffer);
continue;
}
printf("pkt size is %d\n", pkt.size);
count++;
}

av_packet_unref(&pkt);

要注意,采集时有时会出现-35返回,是设备临时不可用,需要忽略,并重试。

记得av_read_frame后要释放对应资源,避免内存泄漏。

AVPacket

头文件:libavcodec/avcodec.h

重要成员

  • data:音视频具体数据。
  • size:缓冲区数据大小。

相关API(成对使用):

如果只在栈空间使用AVPacket,则可以只用以下两个方法。

  • av_init_packet:AVPacket初始化方法,但不会填充datasize
  • av_packet_unref:释放AVPacket资源。内部会释放buffer的内存占用。

堆空间分配:

  • av_packet_alloc:分配空间并初始化AVPacket,同样不会填充data、size。即包含了av_init_packet的调用。
  • av_packet_free:释放AVPacket对应的资源,同样也包含了av_packet_unref调用。

写入到文件

基本步骤:

  1. 创建文件;
  2. 把音频写入到文件中;
  3. 关闭文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建文件,权限:w(写入)b(写入二进制)+(若文件不存在则创建)
char *output_path = "/Users/bq/Workspace/test/audio.pcm";
FILE *output_file = fopen(output_path, "wb+");

while ((ret == 0 || ret == -35) && count < 500) {
ret = av_read_frame(context, &pkt);

printf("[%d] pkt size is %d\n", count, pkt.size);
count++;

// 写入文件
fwrite(pkt.data, pkt.size, 1, output_file);
fflush(output_file);

av_packet_unref(&pkt);
}


// 关闭文件
fclose(output_file);
printf("完成写入");

播放测试:

1
ffplay -ar 44100 -ac 2 -f f32le audio.pcm

编码音频

FFmpeg编码基本过程:

  1. 创建编码器;
  2. 创建上下文;
  3. 打开编码器;
  4. 送数据给编码器;编码器一般是要缓冲一部分帧,才能编码输出帧。
  5. 编码;
  6. 释放资源。

打开编码器API:

  1. avcodec_find_encoder:查找编码器。通过id或名字查找。
  2. avcodec_alloc_context3:创建上下文。
  3. avcodec_open2:打开编码器。

fdk_aac,支持的采样大小是16位的,不能设置为FLT。设置了profile后,需要bit_rate置0,否则profile设置不生效。

传输数据API:

avcodec_send_frame,把帧输入到编码器。顾名思义,其传入的是AVFrame。会先缓冲一部分数据。

avcodec_receive_packet,获取编码后的数据。顾名思义,其输出的是AVPacket。

AVFrame与AVPacket,从命名上看,似乎frame是解压后的帧、packet是压缩后的数据包。之前打开设备并从中av_read_frame出来的却是个packet,这是因为FFmpeg把设备视为媒体文件处理。而从媒体文件读取的就是packet数据。即按照正规流程,从设备读取帧获得packet后,还需要走解码的步骤,最后得出AVFrame。我们是知道从设备读取的帧就是未压缩的帧,所以就直接从packet里面拿数据了。这其实也是种投机取巧的方式。

重采样音频

基本步骤:

  1. 创建重采样上下文;
  2. 设置参数;
  3. 初始化重采样;

对应API:

  1. swr_alloc_set_opts:创建了重采样的上下文,并进行了初始化。
  2. swr_init
  3. swr_convert
  4. swr_free

需要头文件:

libswresample/swresample.h

channel layout:指扬声器的布局,用它来表示声道信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 创建文件,权限:w(写入)b(写入二进制)+(若文件不存在则创建)
char *output_path = "/Users/bq/Workspace/test/audio.pcm";
FILE *output_file = fopen(output_path, "wb+");

// 创建重采样上下文
SwrContext *swr_ctx = swr_alloc_set_opts(NULL, // ctx
AV_CH_LAYOUT_MONO, AV_SAMPLE_FMT_S16, 44100, // 输出格式
AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_FLT, 44100, // 输入格式
0, NULL
);
// 初始化
if (!swr_ctx || swr_init(swr_ctx) < 0) {
printf("重采样上下文创建失败");
}

// 重采样输入数据
const int ch_length = 4096 / 4 / 2;
uint8_t **src_data = NULL;
int src_data_length = 0;
// 根据格式生成缓冲区
av_samples_alloc_array_and_samples(&src_data, &src_data_length, 2, ch_length, AV_SAMPLE_FMT_FLT, 0);

// 重采样输出数据
uint8_t **dst_data = NULL;
int dst_data_length = 0;
// 根据格式生成缓冲区
av_samples_alloc_array_and_samples(&dst_data, &dst_data_length, 1, ch_length, AV_SAMPLE_FMT_S16, 0);

while ((ret == 0 || ret == -35) && count < 500) {
// 读取音频数据
ret = av_read_frame(context, &pkt);

if (ret == -35) continue;
printf("[%d] pkt size is %d\n", count, pkt.size);
count++;

// 拷贝数据到输入
memcpy(src_data[0], pkt.data, pkt.size);

// 重采样,转换的数据量是每个通道的采样数
swr_convert(swr_ctx,
dst_data, 512, // 输出
(const uint8_t **)src_data, 512 // 输入
);

// 写入文件
//fwrite(pkt.data, (size_t)pkt.size, 1, output_file);
fwrite(dst_data[0], 1, dst_data_length, output_file);
fflush(output_file);

av_packet_unref(&pkt);
}

if (src_data) {
av_freep(&src_data[0]);
}
av_freep(&src_data);
if (dst_data) {
av_freep(&dst_data[0]);
}
av_freep(&dst_data);
swr_free(&swr_ctx);

// 关闭文件
fclose(output_file);
printf("完成写入");

播放测试:

1
ffplay -ar 44100 -ac 1 -f s16le audio.pcm

欢迎关注我的其它发布渠道