CarPlay Wireless 使用fdk_aac库解码Raw AAC-LC & AAC-ELD - Go语言中文社区

CarPlay Wireless 使用fdk_aac库解码Raw AAC-LC & AAC-ELD


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个主要问题:

1. 获取音频配置信息AudioSpecificConfig

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 decodern");
    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 extradatan");
      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的问题算是解决了。

2. 解码函数调用不成功问题

正确设置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 endn");
      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的值来进行分配。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/Miss_yuan/article/details/88527874
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2019-10-26 22:54:58
  • 阅读 ( 1858 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

推荐文章

猜你喜欢