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 用纸:胶版纸

Windows 的 ChannelMask 转 ffmpeg 的 ChannelLayout

前情

最近写录音程序,发现 MBP 的扬声器是 4 频道的,然后在抓音频保存时,Opus 编码器居然不支持 4 个频道,avcodec_open2() 会返回错误码 -22,Invalid argument。解决方法就是 resample 成 AV_CH_LAYOUT_STEREO。搞定后就顺便细研了这个 ChannelLayout,UMU 的代码里需要把微软 CoreAudio 的一些参数转成 ffmpeg 的,比如之前写的《Windows 的 WAVEFORMATEX 转 ffmpeg 的 AVSampleFormat 类型》,这次写 ChannelLayout 的转换。

分析

ffmpeg 的 channel layouts 定义:

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
/**
* @}
* @defgroup channel_mask_c Audio channel layouts
* @{
* */
#define AV_CH_LAYOUT_MONO (AV_CH_FRONT_CENTER)
#define AV_CH_LAYOUT_STEREO (AV_CH_FRONT_LEFT|AV_CH_FRONT_RIGHT)
#define AV_CH_LAYOUT_2POINT1 (AV_CH_LAYOUT_STEREO|AV_CH_LOW_FREQUENCY)
#define AV_CH_LAYOUT_2_1 (AV_CH_LAYOUT_STEREO|AV_CH_BACK_CENTER)
#define AV_CH_LAYOUT_SURROUND (AV_CH_LAYOUT_STEREO|AV_CH_FRONT_CENTER)
#define AV_CH_LAYOUT_3POINT1 (AV_CH_LAYOUT_SURROUND|AV_CH_LOW_FREQUENCY)
#define AV_CH_LAYOUT_4POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_CENTER)
#define AV_CH_LAYOUT_4POINT1 (AV_CH_LAYOUT_4POINT0|AV_CH_LOW_FREQUENCY)
#define AV_CH_LAYOUT_2_2 (AV_CH_LAYOUT_STEREO|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
#define AV_CH_LAYOUT_QUAD (AV_CH_LAYOUT_STEREO|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
#define AV_CH_LAYOUT_5POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
#define AV_CH_LAYOUT_5POINT1 (AV_CH_LAYOUT_5POINT0|AV_CH_LOW_FREQUENCY)
#define AV_CH_LAYOUT_5POINT0_BACK (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
#define AV_CH_LAYOUT_5POINT1_BACK (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_LOW_FREQUENCY)
#define AV_CH_LAYOUT_6POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_CENTER)
#define AV_CH_LAYOUT_6POINT0_FRONT (AV_CH_LAYOUT_2_2|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
#define AV_CH_LAYOUT_HEXAGONAL (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_BACK_CENTER)
#define AV_CH_LAYOUT_6POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_CENTER)
#define AV_CH_LAYOUT_6POINT1_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_BACK_CENTER)
#define AV_CH_LAYOUT_6POINT1_FRONT (AV_CH_LAYOUT_6POINT0_FRONT|AV_CH_LOW_FREQUENCY)
#define AV_CH_LAYOUT_7POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
#define AV_CH_LAYOUT_7POINT0_FRONT (AV_CH_LAYOUT_5POINT0|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
#define AV_CH_LAYOUT_7POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
#define AV_CH_LAYOUT_7POINT1_WIDE (AV_CH_LAYOUT_5POINT1|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
#define AV_CH_LAYOUT_7POINT1_WIDE_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
#define AV_CH_LAYOUT_OCTAGONAL (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_CENTER|AV_CH_BACK_RIGHT)
#define AV_CH_LAYOUT_HEXADECAGONAL (AV_CH_LAYOUT_OCTAGONAL|AV_CH_WIDE_LEFT|AV_CH_WIDE_RIGHT|AV_CH_TOP_BACK_LEFT|AV_CH_TOP_BACK_RIGHT|AV_CH_TOP_BACK_CENTER|AV_CH_TOP_FRONT_CENTER|AV_CH_TOP_FRONT_LEFT|AV_CH_TOP_FRONT_RIGHT)
#define AV_CH_LAYOUT_STEREO_DOWNMIX (AV_CH_STEREO_LEFT|AV_CH_STEREO_RIGHT)

ffmpeg 的频道位置信息:

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
#define AV_CH_FRONT_LEFT             0x00000001
#define AV_CH_FRONT_RIGHT 0x00000002
#define AV_CH_FRONT_CENTER 0x00000004
#define AV_CH_LOW_FREQUENCY 0x00000008
#define AV_CH_BACK_LEFT 0x00000010
#define AV_CH_BACK_RIGHT 0x00000020
#define AV_CH_FRONT_LEFT_OF_CENTER 0x00000040
#define AV_CH_FRONT_RIGHT_OF_CENTER 0x00000080
#define AV_CH_BACK_CENTER 0x00000100
#define AV_CH_SIDE_LEFT 0x00000200
#define AV_CH_SIDE_RIGHT 0x00000400
#define AV_CH_TOP_CENTER 0x00000800
#define AV_CH_TOP_FRONT_LEFT 0x00001000
#define AV_CH_TOP_FRONT_CENTER 0x00002000
#define AV_CH_TOP_FRONT_RIGHT 0x00004000
#define AV_CH_TOP_BACK_LEFT 0x00008000
#define AV_CH_TOP_BACK_CENTER 0x00010000
#define AV_CH_TOP_BACK_RIGHT 0x00020000
#define AV_CH_STEREO_LEFT 0x20000000 ///< Stereo downmix.
#define AV_CH_STEREO_RIGHT 0x40000000 ///< See AV_CH_STEREO_LEFT.
#define AV_CH_WIDE_LEFT 0x0000000080000000ULL
#define AV_CH_WIDE_RIGHT 0x0000000100000000ULL
#define AV_CH_SURROUND_DIRECT_LEFT 0x0000000200000000ULL
#define AV_CH_SURROUND_DIRECT_RIGHT 0x0000000400000000ULL
#define AV_CH_LOW_FREQUENCY_2 0x0000000800000000ULL

微软的频道位置信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Speaker Positions for dwChannelMask in WAVEFORMATEXTENSIBLE:
#define SPEAKER_FRONT_LEFT 0x1
#define SPEAKER_FRONT_RIGHT 0x2
#define SPEAKER_FRONT_CENTER 0x4
#define SPEAKER_LOW_FREQUENCY 0x8
#define SPEAKER_BACK_LEFT 0x10
#define SPEAKER_BACK_RIGHT 0x20
#define SPEAKER_FRONT_LEFT_OF_CENTER 0x40
#define SPEAKER_FRONT_RIGHT_OF_CENTER 0x80
#define SPEAKER_BACK_CENTER 0x100
#define SPEAKER_SIDE_LEFT 0x200
#define SPEAKER_SIDE_RIGHT 0x400
#define SPEAKER_TOP_CENTER 0x800
#define SPEAKER_TOP_FRONT_LEFT 0x1000
#define SPEAKER_TOP_FRONT_CENTER 0x2000
#define SPEAKER_TOP_FRONT_RIGHT 0x4000
#define SPEAKER_TOP_BACK_LEFT 0x8000
#define SPEAKER_TOP_BACK_CENTER 0x10000
#define SPEAKER_TOP_BACK_RIGHT 0x20000

结论

经过对比可以发现两者是一致的,只是微软的 dwChannelMask 是 DWORD,ffmpeg 用的是 int64_t。所以我们可以写个函数来转换他们:

1
2
3
4
5
6
7
8
// 微软的 ChannelMask 值转为 ffmpeg 的 channel_layout
inline int64_t GetChannelLayout(const WAVEFORMATEX *wave_format)
{
if (WAVE_FORMAT_EXTENSIBLE == wave_format->wFormatTag) {
return reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(wave_format)->dwChannelMask;
}
return av_get_default_channel_layout(wave_format->nChannels);
}

相关书籍

京东联盟购买链接:

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

UEFI 里的 IGD Minimum Memory 和 IGD Aperture Size

今天进 UEFI 看到集显的两个设置选项:IGD Minimum Memory 和 IGD Aperture Size,想着 UMU 的 NUC 有 32G 内存,要不要改大点?然后搜一下他们的作用,结果发现最好不要改……

知识

  1. Adjusting the minimum memory can impact graphics performance in legacy operating systems (Windows 7/8/8/1).

    The default value (64 MB) is recommended for Windows 10. Windows 10 will allocate graphics memory dynamically when it loads, so setting the IGD minimal memory to higher value may not improve performance.

  2. Keep the default BIOS setting for IGD Aperture Size and IGD Min Memory. This values are used only during POST and to boot of the Windows.

    Window 10 assigns automatically the maximum available graphics memory and it depends off how much RAM you have. Usually it assigns about half of available RAM.

参考

https://communities.intel.com/thread/106880

https://communities.intel.com/thread/106428