AAC 编码之 ADTS 头相关分析

之前在《Opus 编解码遇到的怪事》说过一个因为编码器不同而导致的怪事的解决过程,最近又出现一例类似情况了。

UMU 的任务是把从麦克风采集到的音频数据,直接编码成 AAC,然后用 live555 流化为 RTSP 协议,做服务端。其中涉及到一个 ADTS 头部的问题,理论上有没有 ADTS 都是可以的,各有可行的解决方案。但在阅读其他同事代码的时候,惊讶地发现,他特地把 ADTS 头给去掉了。而 UMU 调试时,发现 AVPacket 的数据里根本没有 ADTS 头,何来去掉之说?

有了上次的经验,UMU 很快推测,我们俩用的编码器可能不同。后来验证,确实如此:ffmpeg 3.1 有两个 AAC 编码器,一个内置的,名字是 aac,另一个第三方的 libfdk_aac,商业使用 non-free。(以前还有其它两个第三方的,因为质量不行,已经被移除,ffmpeg 官网上有说明)默认的编译方式只有前者,后者需要使用 non-free 参数编译,基于后期的版权问题考虑,UMU 使用的是内置的 aac。但为了调查这个问题,UMU 特地编译并使用了 libfdk_aac,发现确实有不同。

  1. aac 编码出来的 AVPacket 是没有 ADTS 头的; libfdk_aac 则有。

  2. aac 不需要设置 profile,因为它默认使用 LC,而 libfdk_aac 支持很多中 profile,所以需要设置一个合适的。

  3. libfdk_aac 设置合适的 profile 字段,编码出来的 AVPacket 有 ADTS 头,VLC 可以播放,特地去掉 ADTS 头,VLC 也可以播放。

  4. 如果不设置 profile,默认是 FF_PROFILE_UNKNOWN,这时有 ADTS 头,但由于这个 ADTS 头里的 adts_buffer_fullness 不对,所以 VLC 无法播放,去掉反而可以。

解决 ffmpeg 与 live555 宏定义冲突

一个工程同时使用了 ffmpeg 和 live555,结果一不注意就混乱了……原因如下:

1
2
3
4
5
6
7
8
9
10
11
// ffmpeg 的 error.h 里 include 了 errno.h,有以下定义:
#define EAGAIN 11


// 而 live555 的 NetCommon.h 里有以下定义:
#ifdef EAGAIN
#undef EAGAIN
#endif

// WSAEWOULDBLOCK == 10035
#define EAGAIN WSAEWOULDBLOCK

很明显,live555 这么做,违背了面向对象的基本特征——封装,这种平台相关的抽象应该封装在源文件里面,而不是放在头文件。挪个位置即可。

跟 UMU 一起玩 OpenWRT(入门篇14):PPTP 穿透

问题

刚刷完 OpenWRT trunk 版本,默认不支持 PPTP passthrough,表现为此路由器内网的 PC 拨号时,认证很快成功,但迟迟不能完成,最终报错误码 619。

原因

这是因为默认不支持 GRE 协议的 NAT。

解决

官方就有解决方案,简单地说是运行一下两条:

1
2
opkg update
opkg install kmod-nf-nathelper-extra

立刻生效。

诗盗·天鸡 Book 测漏

《#诗盗#·天鸡 Book 测漏》:白首相知友难交,一心逍遥,两袖飘飘;十里春风稣不嫖,九零太老,零零太早!

注解

改编自霹雳角色“天迹”的诗号。

仙衣眠云碧岚袍,一襟潇洒,两袖飘飘;
玉墨舒心春酝瓢,行也逍遥,坐也逍遥。

云录音

在云游戏和云桌面项目中,总结了几类声音采集技术,把录音做到极致。

从外设录音

最典型的就是麦克风,内置麦克风、外置麦克风,其实还有一种通过 LineIn 插入的其它播放器设备,比如 CD、DVD 等。

采集这种音频的方法可以只用 ffmpeg 搞定:av_find_input_format(“dshow”)…,也可以用 CoreAudio 搞定:

1
2
enumerator->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, ...
audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK ...

从播放设备回放录音

采集方式是用 CoreAudio:

1
2
enumerator->(eRender, DEVICE_STATE_ACTIVE, ...
audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, ...

这种方式会混音,比如说您开个 foobar 播歌,再开个 QQ 影音看电影,则会录到这两个应用程序的混音,嗯,如果 QQ 再嘀嘀嘀,也是会混进去的……

虚拟声卡采集

有个叫 Virtual Audio Cable 的虚拟声卡,能虚拟多张声卡,并且可以把声音转发到对应的虚拟 LineIn 设备,供应用程序采集。

只录制某个应用程序的音

比前一种更先进一些,多个播放器同时播歌,我们可以只录其中一个。

采集方法是:Hook CoreAudio。

另一个思路是:Hook 到这个应用,给它单独指定一个输出设备,其它应用不能用,否则还是混音了,然后用前面的回放录音技术录制这个独占的输出设备。您可能要说,哪有那么多输出设备?这个问题可以用前面提到的虚拟声卡解决,分分秒虚拟出 64 个是没问题的。而且用 VAC 的好处是,可以在这 64 个对应的 LineIn 通道直接录制,不需要用 CoreAudio,兼容性会更好。

把 ffmpeg AVAudioFifo/AVFrame 数据读到共享内存

一般情况下操作 AVAudioFifo/AVFrame 都是用全套 ffmpeg API,内部自己管理内存,不需要了解它们内部怎么组织内存。比如:

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
inline int InitFrame(AVFrame *&frame, int frame_size = kTargetSamplesPerFrame)
{
frame = av_frame_alloc();
if (nullptr == frame) {
return AVERROR(ENOMEM);
}

frame->nb_samples = frame_size;
frame->channel_layout = av_get_default_channel_layout(kTargetChannels);
frame->format = kTargetSampleFormat;
frame->sample_rate = kTargetSampleRate;

int error = av_frame_get_buffer(frame, 0);
if (error < 0) {
av_frame_free(&frame);
ATLTRACE2(atlTraceException, 0, "!av_frame_get_buffer(), #%d, %s\n", error, GetAvErrorText(error));
}
return error;
}

{
...
AVFrame *frame;
error_code = InitFrame(frame);
if (error_code < 0) {
ATLTRACE2(atlTraceException, 0, __FUNCTION__ ": !InitFrame(), #%d\n", error_code);
return error_code;
}
ON_SCOPE_EXIT([&] {
av_frame_free(&frame);
});

int read_size = av_audio_fifo_read(fifo_, (void **)frame->data, kTargetSamplesPerFrame);
...
}

这里读了一个 AVFrame 出来,并不需要知道具体的内存布局,但如果要写入 FileMapping 对象里,就得知道了! 参考以下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int av_audio_fifo_read(AVAudioFifo *af, void **data, int nb_samples)
{
int i, size;

if (nb_samples < 0)
return AVERROR(EINVAL);
nb_samples = FFMIN(nb_samples, af->nb_samples);
if (!nb_samples)
return 0;

size = nb_samples * af->sample_size;
for (i = 0; i < af->nb_buffers; i++) {
if (av_fifo_generic_read(af->buf[i], data[i], size, NULL) < 0)
return AVERROR_BUG;
}
af->nb_samples -= nb_samples;

return nb_samples;
}

和 AVFrame 定义:

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
typedef struct AVFrame {
#define AV_NUM_DATA_POINTERS 8
/**
* pointer to the picture/channel planes.
* This might be different from the first allocated byte
*
* Some decoders access areas outside 0,0 - width,height, please
* see avcodec_align_dimensions2(). Some filters and swscale can read
* up to 16 bytes beyond the planes, if these filters are to be used,
* then 16 extra bytes must be allocated.
*
* NOTE: Except for hwaccel formats, pointers not needed by the format
* MUST be set to NULL.
*/
uint8_t *data[AV_NUM_DATA_POINTERS];

/**
* For video, size in bytes of each picture line.
* For audio, size in bytes of each plane.
*
* For audio, only linesize[0] may be set. For planar audio, each channel
* plane must be the same size.
*
* For video the linesizes should be multiples of the CPUs alignment
* preference, this is 16 or 32 for modern desktop CPUs.
* Some code requires such alignment other code can be slower without
* correct alignment, for yet other it makes no difference.
*
* @note The linesize may be larger than the size of usable data -- there
* may be extra padding present for performance reasons.
*/
int linesize[AV_NUM_DATA_POINTERS];
...
};

以 AV_SAMPLE_FMT_S16 为例,发现 InitFrame() 里的 av_frame_get_buffer() 之后只有 linesize[0] 是非 0,即 data[0] 的分配长度,其它 7 个都是 0,即 data[1] -> data[7] 都没有分配,于是猜测就是读 data[0],长度 linesize[0],尝试把它写到 FileMapping 里,果然是对的。如果 SampleFormat 是带 P 的,就不是只有 data[0] 了,有几个 channel 就有几个 data,要相应改变。

相关书籍

京东联盟购买链接:

FFmpeg从入门到精通 出版时间:2018-04-01 用纸:胶版纸