CarPlay在Wireless的模式下,音频数据传输不再采用Wired模式下的LPCM格式,而是压缩编码格式。多媒体音频(Main High Audio)采用Raw AAC-LC编码格式,其他类型音频(Main Audio & Alt Audio)可选Raw AAC-ELD或OPUS编码格式。而且由于CarPlay运行时会同时出现输出多路音频流的情况,所以需要支持多解码器实例同时工作。
CarPlay over USB uses LPCM for audio. CarPlay over wireless uses raw AAC-LC for high latency audio (Main High Audio) and either OPUS or raw AAC-ELD for low latency audio (Main Audio except “media” and Alt Audio). Accessories supporting CarPlay over wireless must support multiple decode instances and concurrent decode/encode instances.
因此,考虑采用软件解码的方式来支持CarPlay Wireless的音频解码功能。常见的AAC软解码库有faad2和fdk_aac。
faad2库仅能支持包含AAC-LC的少数profile解码,而fdk_aac库是ffmpeg项目中推荐采用的高精度AAC编解码库,可以支持的profile更多,所以打算使用fdk_aac库来实现AAC-LC和AAC-ELD解码。
FAAD2的主页
GitHub上fdk-aac的项目主页
fdk-aac的介绍界面
fdk_aac源码包下载
fdk-aac解码库的API使用,可以参考其自带的文档./fdk-aac-2.0.0/documentation/aacDecoder.pdf
大致的调用流程就是:
aacDecoder_Open();
aacDecoder_ConfigRaw();
loop{
aacDecoder_Fill();
aacDecoder_DecodeFrame();
}
aacDecoder_Close();
这里记录一些自己解码过程中遇到的2个主要问题:
AudioSpecificConfig 定义于文档 ISO/IEC 14496-3中。如下:
AudioSpecificConfig包含流的配置信息,对于解码处理和解析Raw数据流是必须的。(其实就类似于咱们解码H.264视频流时需要提供的SPS和PPS信息一样)
从aacDecoder.pdf中文档可以看到相关说明,在调用解码API aacDecoder_DecodeFrame()之前需要进行配置:
这里说没有外带ASC或者SMC数据的话,可以不必使用aacDecoder_ConfigRaw()进行设置,aacDecoder_DecodeFrame()执行中会自己配置。但其实如果是Raw Data的话,是必须要设置的,否则解码不成功。这个看函数说明可以知道,如下:
看说明的话,我目前处理的AAC Raw Data需要提供的ASC格式的信息。但在CarPlay Wireless中手机端似乎无法直接获取到AAC Raw Data对应的信息数据块。网上找了一些ASC的格式相关的说明,如下:
标准文档里有整体的语法定义,但是不完整,一些新的Audio Object Type(数值超过5个bits的)没有加入进来。AAC-LC需要对应的GASpecificConfig也没在这里定义。
audioObjectType,samplingFrequencyIndex,channelConfiguration这3个字段的完整的配置可以参考这里:
Audio_Specific_Config
后续的部分字段可以参考这里:
Understanding_AAC
有个可以参考的GASpecificConfig信息设置:
Microsoft aac-decoder
我没有在网上找到ASC完整的定义,所以自己构建ASC数据的话,可能还是有一些问题。(AAC-LC的ASC相对比较简单,GASpecificConfig中3个bits填充0即可,测试可以正常解码)
另外,有个曲线救国的办法来解决ASC的问题,先使用包含fdk_aac编解码库的ffmpeg工具转码一个指定AAC格式的文件,然后来用ffmpeg工具把音频信息中的extradata给dump出来。从ffmpeg源码中调用fdk_aac的地方可以得知,AVCodecContext结构中的extradata就是aacDecoder_ConfigRaw()需要的数据,如下:
static av_cold int fdk_aac_decode_init(AVCodecContext* avctx)
{
FDKAACDecContext* s = avctx->priv_data;
AAC_DECODER_ERROR err;
s->handle = aacDecoder_Open(avctx->extradata_size ? TT_MP4_RAW : TT_MP4_ADTS, 1);
if(!s->handle) {
av_log(avctx, AV_LOG_ERROR, "Error opening decoder\n");
return AVERROR_UNKNOWN;
}
if(avctx->extradata_size) {
if((err = aacDecoder_ConfigRaw(s->handle, &avctx->extradata,
&avctx->extradata_size)) != AAC_DEC_OK) {
av_log(avctx, AV_LOG_ERROR, "Unable to set extradata\n");
return AVERROR_INVALIDDATA;
}
}
...
}
我在进行解码功能验证前,在CarPlay Wireless代码接收RTP数据的位置分别dump出了 AAC-LC/44KHZ/STEREO 和 AAC-ELD/16KHZ/MONO 的数据到文件。比如想获取 AAC-ELD/16KHZ/MONO 这个格式的ASC可以这样:
lzy@~/GitHub/ffmpeg$ ./ffmpeg -i 1.mp3 -acodec libfdk_aac -ar 16000 -ac 1 -profile:a aac_eld aac_eld_16k_ch1.m4a
lzy@~/GitHub/ffmpeg$ ./ffprobe -show_data -show_streams aac_eld_16k_ch1.m4a
##省略了部分输出
[STREAM]
index=0
codec_name=aac
codec_long_name=AAC (Advanced Audio Coding)
profile=ELD
codec_type=audio
codec_time_base=1/16000
codec_tag_string=mp4a
nb_frames=9014
extradata=
00000000: f8f0 2000 .. .
[/STREAM]
可以看到extradata数据为: 0xF8 0xF0 0x20 0x00。至此,AudioSpecificConfig的问题算是解决了。
正确设置ASC之后,按照文档说明,循环读取dump出来的AAC文件内容进行解码,但是aacDecoder_DecodeFrame()会直接返回AAC_DEC_UNKNOWN错误。因为aacDecoder_ConfigRaw()设置完ASC之后,使用aacDecoder_GetStreamInfo()获取信息可以看到参数解析的都是正确的。最后还是看文档发现问题所在:
文档里有说明,如果Raw Data格式一次只能装载一帧数据去解码。因为我在使用fdk-aac解码之前,使用FAAD2的API已经成功完成了对dump出来的AAC-LC数据的解码。而在使用FAAD2时,数据都是整块丢进解码器的,所以这里使用fdk-aac时,也想当然了。(使用开源库还是需要仔细阅读以下API文档,很重要!)
怎么划分每一帧AAC数据呢,对于AAC-lC的Raw Data格式,有个工具可以比较直观的看出来(不支持AAC-LED),可以用雷神的这个AAC解析器:
视音频编解码学习工程:AAC格式分析器
我自己dump出来AAC-LC数据解析后是这样,可以看到每一帧的Size:
当然这个工具只能用于辅助分析。经过比较Size数值可以发现,这里的每一帧大小和CarPlay Wireless模式下,每次音频送入解码器之前的Size是一致的。(CarPlay的RTP payload数据还需要经过一次Decrypt才是Raw ACC Data)
这样的话,其实只需要按照Decrypt之后的数据块大小就可以使用fdk-aac解码了。为了最小化我的解码功能测试,我又按照frame块大小重新dump了数据进行测试(每个frame一个单独的文件)。
解码的整理流程如下:
int main()
{
AAC_DECODER_ERROR err = AAC_DEC_OK;
HANDLE_AACDECODER decoder = aacDecoder_Open(TT_MP4_RAW, 1);
assert(decoder);
// 设置ASC信息
if (g_decode_aac_profile == AOT_ER_AAC_ELD) {
UCHAR conf[] = {0xF8, 0xF0, 0x20, 0x00}; //AAL-ELD 16000kHz MONO
UCHAR* conf_array[1] = { conf };
UINT length = 4;
err = aacDecoder_ConfigRaw(decoder, conf_array, &length);
assert(!err);
} else if (g_decode_aac_profile == AOT_AAC_LC) {
UCHAR conf[] = {0x12, 0x10}; //AAL-LC 44100kHz STEREO
UCHAR* conf_array[1] = { conf };
UINT length = 2;
err = aacDecoder_ConfigRaw(decoder, conf_array, &length);
assert(!err);
}
// 获取信息
CStreamInfo* info = aacDecoder_GetStreamInfo(decoder);
assert(info);
// 构建AAC Raw Data Buffer List
prepare_aac_raw_buf_list();
// pcm输出buffer的大小可以参考CStreamInfo中frameSize的定义
// typedef struct {
// ...
// INT frameSize; /*!< The frame size of the decoded PCM audio signal. \n
// Typically this is: \n
// 1024 or 960 for AAC-LC \n
// 2048 or 1920 for HE-AAC (v2) \n
// 512 or 480 for AAC-LD and AAC-ELD \n
// 768, 1024, 2048 or 4096 for USAC */
//...
//}CStreamInfo
int max_frame_size;
if (g_decode_aac_profile == AOT_ER_AAC_ELD) {
max_frame_size = 512;
} else if (g_decode_aac_profile == AOT_AAC_LC) {
max_frame_size = 1024;
}
int pcm_buffer_size = max_frame_size * MAX_CHANNELS;
INT_PCM* pcm_buffer = (INT_PCM*)malloc(sizeof(INT_PCM) * pcm_buffer_size);
assert(pcm_buffer);
FILE* output_fp = fopen(PCM_OUTPUT_FILE_NAME, "wb");
assert(output_fp);
// 开始解码循环
int cur_raw_buf_index = 0;
UINT flags = 0;
UINT bytes_valid;
do {
if (cur_raw_buf_index >= raw_buf_total_cout) {
printf("decode end\n");
break;
}
// 加载本次需要解码的数据
bytes_valid = g_raw_buf_size_list[cur_raw_buf_index];
err = aacDecoder_Fill(decoder, &(g_raw_buf_list[cur_raw_buf_index]), &(g_raw_buf_size_list[cur_raw_buf_index]), &bytes_valid);
assert(err == AAC_DEC_OK);
// 解码
err = aacDecoder_DecodeFrame(decoder, pcm_buffer, pcm_buffer_size / sizeof(INT_PCM), flags);
// 因为这里我们解码的是Raw Data,每次送入一帧后就可以解码完成返回AAC_DEC_OK
// 对于非Raw Data 的情况需要针对返回值进行处理,如出错处理,数据不够的处理
assert(err == AAC_DEC_OK);
// 通过获取信息,计算实际输出的pcm数据大小
CStreamInfo* info = aacDecoder_GetStreamInfo(decoder);
assert(info);
int output_pcm_bytes = info->frameSize * info->numChannels * 2;
// pcm 数据写入文件
if (output_fp) {
size_t ws = fwrite(pcm_buffer, output_pcm_bytes, 1, output_fp);
assert(ws > 0);
}
cur_raw_buf_index++;
} while (1);
// 释放资源
if (output_fp) {
fclose(output_fp);
}
if (pcm_buffer) {
free(pcm_buffer);
}
release_aac_raw_buf_list();
aacDecoder_Close(decoder);
return 0;
}
完整代码和测试用的ACC Raw Data放在这里了:
https://github.com/lzy831/demo/tree/master/fdk_aac_decode_raw
2019-3-22 补充:
随着CarPlay Wireless的开发,遇到了一个新问题。
ACC-ELD数据每次解码出来的framesize是512或者480,这个从下面framesize的注释可以看出来:
/**
* \brief This structure gives information about the currently decoded audio
* data. All fields are read-only.
*/
typedef struct {
...
INT frameSize; /*!< The frame size of the decoded PCM audio signal. \n
Typically this is: \n
1024 or 960 for AAC-LC \n
2048 or 1920 for HE-AAC (v2) \n
512 or 480 for AAC-LD and AAC-ELD \n
768, 1024, 2048 or 4096 for USAC */
...
INT aacSamplesPerFrame; /*!< Samples per frame for the AAC core (from ASC)
divided by a (ELD) downscale factor if present. \n
Typically this is (with a downscale factor of 1):
\n 1024 or 960 for AAC-LC \n 512 or 480 for
AAC-LD and AAC-ELD */
...
} CStreamInfo;
而CarPlay Wireless中手机端发过来的ACC-ELD数据默认是固定按照480来处理的。(RTP包的timestamp值,解码数据存放的缓存都是根据这个值来设置的),所以我需要设置解码器来输出匹配的数据量。
上面代码中aacSamplesPerFrame的注释中可以看出,framesize是从ASC中获取的。但是我实在是找不到ASC对应的文档了。只能从代码里看:
static TRANSPORTDEC_ERROR EldSpecificConfig_Parse(CSAudioSpecificConfig *asc,
HANDLE_FDK_BITSTREAM hBs,
CSTpCallBacks *cb) {
...
FDKmemclear(esc, sizeof(CSEldSpecificConfig));
esc->m_frameLengthFlag = FDKreadBits(hBs, 1);
if (esc->m_frameLengthFlag) {
asc->m_samplesPerFrame = 480;
} else {
asc->m_samplesPerFrame = 512;
}
...
}
可以看出来AAC-ELD对应的配置是EldSpecificConfig(AAC-LC对应的是GaSpecificConfig),而samplesPerFrame的值是由EldSpecificConfig中的samplesPerFrame这个bit决定的。然后我查了一下代码,这个bit就是紧跟着channelConfiguration这个域后面的一个bit。
进行正确的设置后,问题解决。
另外发现的一个小问题是在aacDecoder_ConfigRaw配置成功ASC后,CStreamInfo.frameSize的值并不会立即更新为正确的framesize,需要在第一次aacDecoder_DecodeFrame解码之后才会更新。所以提前分配输出缓存的话,可以在aacDecoder_ConfigRaw之后参考CStreamInfo.aacSamplesPerFrame的值来进行分配。