初次发布文章时的版本为faac-1.29.9.2
本文所提及的优化方法已经在gitee创建相应工程,此工程中使用的版本为faac-1.30,链接如下
gitee.com/dma/faac-memory-optimization
详细使用方法参见例程 frontend\main.c
简易demo如下
// 常用PCM都是 int16_t 类型,这里使用 sizeof(int16_t) 申请采样点的内存
channel = 2; // 声道数
hEncoder = faacEncOpen(44100, channel, &inputSamples, &maxOutputBytes);
in_buf16 = malloc(inputSamples * sizeof(int16_t));
out_buf = malloc(maxOutputBytes);
faacEncConfigurationPtr config = faacEncGetCurrentConfiguration(hEncoder);
config->mpegVersion = MPEG4;
config->aacObjectType = LOW;
// config->jointmode = JOINT_NONE;
// config->useLfe = 0;
// config->useTns = 0;
config->bitRate = 64000;
config->bandWidth = 32000;
// config->quantqual = 0;
config->outputFormat = ADTS_STREAM;
config->inputFormat = FAAC_INPUT_16BIT;
// config->pnslevel = 0;
if (!faacEncSetConfiguration(hEncoder, config))
{
fprintf(stderr, "Unsupported output format!\n");
return 1;
}
i = 0;
while (i++ < 1000)
{
fread(in_buf16, sizeof(int16_t), inputSamples * channel, fpr);
enc_out_len = faacEncEncode(hEncoder, in_buf16, inputSamples, out_buf, maxOutputBytes);
fwrite(out_buf, 1, enc_out_len, fpw);
}
faacEncClose(hEncoder);
因windows下编译需要,使用cmake重新编写
脚本如下
cmake_minimum_required(VERSION 2.6)
project (faac_static LANGUAGES C)
add_compile_options(-Wall -O2 -g)
set(CMAKE_BUILD_TYPE "Debug")
add_compile_definitions(
# HAVE_IMMINTRIN_H
# HAVE_INTTYPES_H
# HAVE_STDINT_H
# HAVE_SYS_TYPES_H
)
list(APPEND FAAC_SOURCE
libfaac/bitstream.c
libfaac/blockswitch.c
libfaac/channels.c
libfaac/fft.c
libfaac/filtbank.c
libfaac/frame.c
libfaac/huff2.c
libfaac/huffdata.c
libfaac/quantize.c
libfaac/stereo.c
libfaac/tns.c
libfaac/util.c
libfaac/kiss_fft/kiss_fft.c
libfaac/kiss_fft/kiss_fftr.c
)
include_directories(./include)
include_directories(./libfaac)
include_directories(./libfaac/kiss_fft)
add_library(faac STATIC ${FAAC_SOURCE})
使用官方脚本编译会生成一个 config.h 其中有一些宏定义,大部分都没用,主要注意一下几个,可以根据需要新建一个config.h之类的手动配一下,或者加到CMAKE的宏里面。
都没什么特殊的,唯一注意的是如果不使用 DRM,可以把这个宏去掉,同时cmake里面源文件把kiss_fft.c、kiss_fftr.c去掉,编译出来的体积会稍微小一点
/* Define if you want to encode for DRM */
/* #undef DRM */
/* Define to 1 if you have the header file. */
#define HAVE_IMMINTRIN_H 1
/* Define to 1 if you have the header file. */
#define HAVE_INTTYPES_H 1
/* Define to 1 if you have the header file. */
#define HAVE_STDINT_H 1
/* Define to 1 if you have the header file. */
#define HAVE_SYS_TYPES_H 1
/* Define to the version of this package. */
#define PACKAGE_VERSION "1.29.9.2"
编译过程中报错,提示
error: unknown type name ‘__m128’
两种解决办法:
# include
immintrin.h
(比如ARM架构),按理说不会有这个错,可以用笨办法把 quantize.c 中 __SSE2__
宏之间的相关代码都删掉libfaac\coder.h
中有一处宏定义 #define MAX_CHANNELS 64
,一般来说最常用的是立体声,对于我来说这里配置为2就够用了。
默认64会申请11MB多的内存,开销非常大!设置为2只有360KB多的内存。
以下结构 faacEncStruct.coderInfo->bwpInfo
中 bwpInfo 成员代码中没有用到,不知道作者为什么没有删除,意义不明,这个去掉每声道可以节约将近 157 KB 内存,非常可观。
faacEncEncode()
的代码中有这样一段
if (!hEncoder->sampleBuff[channel])
hEncoder->sampleBuff[channel] = (double*)AllocMemory(FRAME_LEN*sizeof(double));
tmp = hEncoder->sampleBuff[channel];
hEncoder->sampleBuff[channel] = hEncoder->nextSampleBuff[channel];
hEncoder->nextSampleBuff[channel] = hEncoder->next2SampleBuff[channel];
hEncoder->next2SampleBuff[channel] = hEncoder->next3SampleBuff[channel];
hEncoder->next3SampleBuff[channel] = tmp;
申请了4个sampleBuff,这个函数每调用一次会依次交换这4个buffer,实际代码中只用到了 sampleBuff 和 next3SampleBuff,不明白作者为什么这样写,也可能是忘了删,这里可以修改为
hEncoder->sampleBuff[channel] = hEncoder->next3SampleBuff[channel];
hEncoder->next3SampleBuff[channel] = tmp;
这样每声道可以节约16KB内存
CoderInfo
中有这样一个成员struct {
int data;
int len;
} s[DATASIZE];
它用来进行哈夫曼编码,查看源码可知里面的数据来自于哈夫曼编码表,哈夫曼编码表的成员原型如下
typedef struct {
const uint16_t len;
const uint16_t data;
} hcode16_t;
因此可以把int改成short,每声道可以节约6KB内存
CoderInfo.tnsInfo
成员也去掉,并删除 faacEncEncode()
中 TnsEncode()
的调用以及两处 TnsInit()
调用。WriteTNSData()
也要做相关修改,使用以下代码替换,同时删除 return bits;
语句后的所有代码 // TnsInfo* tnsInfoPtr = &coderInfo->tnsInfo;
#ifndef DRM
if (writeFlag) {
// PutBit(bitStream,tnsInfoPtr->tnsDataPresent,LEN_TNS_PRES);
PutBit(bitStream,0,LEN_TNS_PRES);
}
bits += LEN_TNS_PRES;
#endif
return bits;
// 这之后的部分可以全部删除
初始化时每声道可以节约大概14KB内存。
经过以上优化魔改,faac的内存开销已经从最初的接近12MB降到了100KB左右!
到此为止,基本上已经没有什么优化空间了,100KB左右的内存开销即使放到stm32的部分中高端型号上都能运行,还要啥自行车?
当然,大家如果有更好的优化方法欢迎留言,或者开源分享你的代码。
以下无关紧要,能优化一点点
faacEncConfiguration
中 int channel_map[64];
可以改为 int channel_map[MAX_CHANNELS];
faacEncStruct
去掉 double *msSpectrum[MAX_CHANNELS];
最近比较闲,所以专门抽时间来看了一下两位网友留言的问题,并进行详细分析。
这确实是一个不合理,但碰巧能够正常运行的优化,下面来详细解释一下这个问题。
来看一下 asd451006071 和 weixin_43957341 这两位网友的留言
asd451006071
2022.10.04
并不是,音频会卡顿,不连续,通过QQ音乐等软件都能听得出来。这是因为huff编码哪里出了问题。我也是查了源码确实huffcode是16位的。但是就是这样。把uint16_t改成int16_t就行了。至于为什么这样就行了。我也感到很好奇。很惊讶。。
weixin_43957341
2021.11.06
第六点,所有 double 转 float 这个有点小坑,虽然看起来能播,单独放在 苹果设备 上也能播,但是封装到 MP4 里,在 苹果设备 上就会播放异常,出现如卡视频,音频只有前几秒声音的情况,搞得我一度怀疑是时间戳或者 MP4 库本身的兼容问题
asd451006071
2022.10.04
huffcode那个s数组,uint16_t改int16_t就好了。你试试看。。
不知道这两位网友是不是看错了,我前文写的很清楚,把这个结构体中的int
改成short
,结果他俩都改成unsigned short
struct {
int data;
int len;
} s[DATASIZE];
于是就会出现音频卡顿的问题,这个和播放器无关,因为就是编码出错了。这份代码我自己也一直再在用,没有任何异常,最近闲下来了,正好研究一下这个问题。
下文都用uint16代替unsigned short,其他数据类似
首先我要承认,这是我的错,这个结构体确实只在 huffcode()
函数中赋值,里面的数据来也确实来自于哈夫曼编码表 book01
到 book11
,我最初在做优化时大概看了一眼这几个编码表,以为数值都在 int16
范围内不会溢出,所以大胆地将 int
改成 short
而且也没出问题。但不巧遗漏了 book03
的倒数第7项 {16,65534}
,这是唯一一个超出 int16
范围的数据,这也是我在重新研究这个问题时才发现的。但这并不是唯一会导致bug的值,但为什么改成int16
正常,改成uint16
反而异常?下面会结合代码进行分析。
先以 uint16
的情况为例,来看实际在 huffcode()
函数中用到 book03
的这段代码
case 3:
case 4:
for(ofs = 0; ofs < len; ofs += 4)
{
// 此处省略若干代码
else
{
data = book[idx].data;
// add sign bits
for(cnt = 0; cnt < 4; cnt++)
{
if(qp[cnt])
{
blen++;
data <<= 1;
if (qp[cnt] < 0)
data |= 1;
}
}
coder->s[datacnt].data = data;
coder->s[datacnt++].len = blen;
DRMDATA;
}
bits += blen;
}
break;
data = book[idx].data;
这里读取的是 {16,65534}
,此时 data
为 65534if(qp[cnt])
条件成立,执行 blen++; data <<= 1
这两句以后,此时 data
为 131068(0x0001 fffc),blen
为 17,这里暂不考虑 if (qp[cnt] < 0)
uint16
溢出,这时 coder->s[datacnt].data = data;
使 s[datacnt].data
被赋值为 65532(0xfffc)最终编码时在 WriteSpectralData()
函数中
static int WriteSpectralData(CoderInfo *coderInfo,
BitStream *bitStream,
int writeFlag)
{
int i, bits = 0;
if (writeFlag) {
for(i = 0; i < coderInfo->datacnt; i++) {
int data = coderInfo->s[i].data;
int len = coderInfo->s[i].len;
if (len > 0) {
PutBit(bitStream, data, len);
bits += len;
}
}
} else {
for(i = 0; i < coderInfo->datacnt; i++) {
bits += coderInfo->s[i].len;
}
}
return bits;
}
int data = coderInfo->s[i].data;
读取的 data
为 65532(0x0000 fffc),len
为17PutBit(bitStream, data, len);
将 17 位数据写入文件,即写入的二进制数据为 0 1111 1111 1111 1100,注意这里写入的最高位是0接下来以 int16
的情况再看一遍这些代码的执行结果
data = book[idx].data;
这里读取的是 {16,65534}
,此时 data
为 65534if(qp[cnt])
条件成立,执行 blen++; data <<= 1
这两句以后,此时 data
为 131068(0x0001 fffc),blen
为 17,这里暂不考虑 if (qp[cnt] < 0)
int16
溢出,这时 coder->s[datacnt].data = data;
使 s[datacnt].data
被赋值为 -4(0xfffc)。WriteSpectralData()
函数中int data = coderInfo->s[i].data;
读取的 data
为 -4(0xffff fffc),len
为17。注意!这里是重点!因为它是有符号数,高位全部被置为1PutBit(bitStream, data, len);
将 17 位数据写入文件,即写入的二进制数据为 1 1111 1111 1111 1100,正好将正确的数值写了进去!所以,真正会出问题的哈夫曼编码不止 {16,65534}
,假设循环执行了4次,也就是放大了16倍,那么凡是大于4096的像 {13,8188}
这样的编码都会出错。另外一个可以使它正常工作的巧合在于查看 huffdata.c
中的编码表会发现,所有数值都是接近2的n次方的数值,对于大于4096的数来说这些数的高4位都是1,使得它即使左移4位,超出16位以上的部分仍然是1,进而在之后转换为有符号数时不会出现该是0的位被补为1,保证了数值的正确。例如8188(0x1ffc),左移4位得131068(0x1fffc),int16
溢出后为-4(0xfffc),再赋值给int32
为-4(0xffff fffc),丝毫不影响。假设出现 4097(0x1001)这样的数,左移4位得65552(0x10010),int16
溢出后为16(0x0010),再赋值给int32
为16(0x0000 0010),数据又会出现错误!
到此为止整个问题分析完毕。一个不合理的优化在两种巧合的共同作用下让它完美运行。
最后再次感谢 asd451006071 和 weixin_43957341 这两位网友的留言!