云录音

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

从外设录音

最典型的就是麦克风,内置麦克风、外置麦克风,其实还有一种通过 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,要相应改变。

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);
}

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

Windows 的 ChannelMask 转 ffmpeg 的 ChannelLayout

需求

Windows 平台,录音。

任务

用 Windows 的 IAudioCaptureClient 对象采集音频,然后用 ffmpeg 编码。

困难

一些类型定义不一样,比如 SampleFormat。

解决方案

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
inline AVSampleFormat GetSampleFormat(const WAVEFORMATEX *wave_format)
{

switch (wave_format->wFormatTag) {
case WAVE_FORMAT_PCM:
if (16 == wave_format->wBitsPerSample) {
return AV_SAMPLE_FMT_S16;
}
if (32 == wave_format->wBitsPerSample) {
return AV_SAMPLE_FMT_S32;
}
break;
case WAVE_FORMAT_IEEE_FLOAT:
return AV_SAMPLE_FMT_FLT;
case WAVE_FORMAT_ALAW:
case WAVE_FORMAT_MULAW:
return AV_SAMPLE_FMT_U8;
case WAVE_FORMAT_EXTENSIBLE:
{
const WAVEFORMATEXTENSIBLE *wfe = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(wave_format);
if (KSDATAFORMAT_SUBTYPE_IEEE_FLOAT == wfe->SubFormat) {
return AV_SAMPLE_FMT_FLT;
}
if (KSDATAFORMAT_SUBTYPE_PCM == wfe->SubFormat) {
if (16 == wave_format->wBitsPerSample) {
return AV_SAMPLE_FMT_S16;
}
if (32 == wave_format->wBitsPerSample) {
return AV_SAMPLE_FMT_S32;
}
}
break;
}
default:
break;
}
return AV_SAMPLE_FMT_NONE;
}

Opus 编解码遇到的怪事

前情

最近参考 ffmpeg 的 transcoding_aac 示例代码,写了一个 transcoding_opus,并拿 MP3 测试转码,结果发现转完的 opus 文件的 SampleFormat 和指定的并不一样。UMU 的代码是把源文件解码出来的 sample 先 resample 成 AV_SAMPLE_FMT_S16 格式,然后再交给 opus encoder 去编码的,但是编完用 ffprobe 查看,发现 SampleFormat 变成 AV_SAMPLE_FMT_FLTP。

那么第一个问题来了,为什么会这样?

分析

开始研究,首先 UMU 把 opus encoder 支持的 sample_fmt 打印出来,发现只有两种:AV_SAMPLE_FMT_S16、AV_SAMPLE_FMT_FLT,压根就没有 AV_SAMPLE_FMT_FLTP,强行指定 AV_SAMPLE_FMT_FLTP 之后,直接报错,不支持这种 sample_fmt。

推测,真的被编码为 AV_SAMPLE_FMT_S16 了,是 ffprobe 的问题,于是自己写了个简化版的 ffprobe,流程几乎是一样的,出来的结果——果然一模一样……打印出 AV_SAMPLE_FMT_FLTP。

接着怀疑 ffprobe 用的 decoder,于是去看了 avcodec_find_decoder 返回的 AVCodec,打印一下 name 和 long_name,和 transcoding_opus 的 avcodec_find_encoder 返回的一比,果然不一样……

选用的编码器是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
AVCodec ff_libopus_encoder = {
.name = "libopus",
.long_name = NULL_IF_CONFIG_SMALL("libopus Opus"),
.type = AVMEDIA_TYPE_AUDIO,
.id = AV_CODEC_ID_OPUS,
.priv_data_size = sizeof(LibopusEncContext),
.init = libopus_encode_init,
.encode2 = libopus_encode,
.close = libopus_encode_close,
.capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_SMALL_LAST_FRAME,
.sample_fmts = (const enum AVSampleFormat[]){ AV_SAMPLE_FMT_S16,
AV_SAMPLE_FMT_FLT,
AV_SAMPLE_FMT_NONE },
.supported_samplerates = libopus_sample_rates,
.priv_class = &libopus_class,
.defaults = libopus_defaults,
};

而选用的解码器是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
AVCodec ff_opus_encoder = {
.name = "opus",
.long_name = NULL_IF_CONFIG_SMALL("Opus"),
.type = AVMEDIA_TYPE_AUDIO,
.id = AV_CODEC_ID_OPUS,
.defaults = opusenc_defaults,
.priv_class = &opusenc_class,
.priv_data_size = sizeof(OpusEncContext),
.init = opus_encode_init,
.encode2 = opus_encode_frame,
.close = opus_encode_end,
.caps_internal = FF_CODEC_CAP_INIT_THREADSAFE | FF_CODEC_CAP_INIT_CLEANUP,
.capabilities = AV_CODEC_CAP_EXPERIMENTAL | AV_CODEC_CAP_SMALL_LAST_FRAME | AV_CODEC_CAP_DELAY,
.supported_samplerates = (const int []){ 48000, 0 },
.channel_layouts = (const uint64_t []){ AV_CH_LAYOUT_MONO,
AV_CH_LAYOUT_STEREO, 0 },
.sample_fmts = (const enum AVSampleFormat[]){ AV_SAMPLE_FMT_FLTP,
AV_SAMPLE_FMT_NONE },
};

问题清楚了,看来用 ID 查找编解码器并不靠谱,因为这个 ID 是 Type ID,不是 Item ID,还是改为用 name 来找:

1
2
//AVCodec *output_codec = avcodec_find_encoder(AV_CODEC_ID_OPUS);
AVCodec *output_codec = avcodec_find_encoder_by_name("opus");

那么,第二个问题顺势而来——哪个比较牛?

结论

用 AV_SAMPLE_FMT_FLTP 后 frame_size 是 120,用其它是 960,frame_size 小有小的好处,比如在做实时编码直播时,理论延迟会更小。

经过测试,用 AV_SAMPLE_FMT_FLTP 的 opus 比 libopus 压缩率普遍略高一些,但它只支持 48000Hz 一种 sample_rate,libopus 支持的更多:48000, 24000, 16000, 12000, 8000。