iOS的AAC编码解析(一) —— 基本概览

版本记录

版本号 时间
V1.0 2017.12.31

前言

AAC(Advanced Audio Coding),中文名:高级音频编码,出现于1997年,基于MPEG-2的音频编码技术。由Fraunhofer IIS、杜比实验室AT&TSony等公司共同开发,目的是取代MP3格式。2000年,MPEG-4标准出现后,AAC重新集成了其特性,加入了SBR技术和PS技术,为了区别于传统的MPEG-2 AAC又称为MPEG-4 AAC

PCM编码

脉冲编码调制(Pulse Code Modulation,PCM),由A.里弗斯于1937年提出的,这一概念为数字通信奠定了基础,60年代它开始应用于市内电话网以扩充容量,使已有音频电缆的大部分芯线的传输容量扩大24~48倍。到70年代中、末期,各国相继把脉码调制成功地应用于同轴电缆通信、微波接力通信、卫星通信和光纤通信等中、大容量传输系统。80年代初,脉冲编码调制已用于市话中继传输和大容量干线传输以及数字程控交换机,并在用户话机中采用。

在光纤通信系统中,光纤中传输的是二进制光脉冲“0”码和“1”码,它由二进制数字信号对光源进行通断调制而产生。而数字信号是对连续变化的模拟信号进行抽样、量化和编码产生的,称为PCM(Pulse-code modulation),即脉冲编码调制。这种电的数字信号称为数字基带信号,由PCM电端机产生。现在的数字传输系统都是采用脉码调制(Pulse-code modulation)体制。PCM最初并非传输计算机数据用的,而是使交换机之间有一条中继线不是只传送一条电话信号。PCM有两个标准(表现形式)即E1和T1。

中国采用的是欧洲的E1标准。T1的速率是1.544Mbit/s,E1的速率是2.048Mbit/s。

脉冲编码调制可以向用户提供多种业务,既可以提供从2M到155M速率的数字数据专线业务,也可以提供话音、图象传送、远程教学等其他业务。特别适用于对数据传输速率要求较高,需要更高带宽的用户使用。

自然界中的声音非常复杂,波形极其复杂,通常我们采用的是脉冲代码调制编码,即PCM编码。PCM通过抽样、量化、编码三个步骤将连续变化的模拟信号转换为数字编码。

  • 抽样:对模拟信号进行周期性扫描,把时间上连续的信号变成时间上离散的信号;
  • 量化:用一组规定的电平,把瞬时抽样值用最接近的电平值来表示,通常是用二进制表示;
  • 编码:用一组二进制码组来表示每一个有固定电平的量化值;

iOS中AAC编码情况

iOS平台支持AAC编码器,主要使用AudioToolbox中的AudioConverter API。之所以做AAC编码器是因为在做一个HLS的功能,HLS要求的TS文件,需要视频采用H264编码,音频采用AAC编码。H264可以使用硬件或软件编码器,前面已经介绍。AAC也可以使用硬件或者软件编码,iOS全都支持。

AAC是一种专为声音数据设计的文件压缩格式。与MP3不同,它采用了全新的算法进行编码,更加高效,具有更高的“性价比”。利用AAC格式,可使人感觉声音质量没有明显降低的前提下,更加小巧。苹果ipod诺基亚手机支持AAC格式的音频文件。

  • 优点:相对于mp3,AAC格式的音质更佳,文件更小。
  • 不足:AAC属于有损压缩的格式,与时下流行的APEFLAC无损格式相比音质存在“本质上”的差距。加之,传输速度更快的USB3.0和16G以上大容量MP3正在加速普及,也使得AAC头上“小巧”的光环不复存在。

iOS上把PCM音频编码成AAC音频流

  • 设置编码器(codec),并开始录制;
  • 收集到PCM数据,传给编码器;
  • 编码完成回调callback,写入文件。

具体原理如下所示:


创建并配置AVCaptureSession

创建AVCaptureSession,然后找到音频的AVCaptureDevice,根据音频device创建输入并添加到session,最后添加output到session。audioFileHandleNSFileHandle,用户写入编码后的AAC音频到文件。

- (void)startCapture 
{
    self.mCaptureSession = [[AVCaptureSession alloc] init];
    mCaptureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    mEncodeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
    AVCaptureDevice *audioDevice = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] lastObject];
    self.mCaptureAudioDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:nil];
    if ([self.mCaptureSession canAddInput:self.mCaptureAudioDeviceInput]) {
        [self.mCaptureSession addInput:self.mCaptureAudioDeviceInput];
    }
    self.mCaptureAudioOutput = [[AVCaptureAudioDataOutput alloc] init];
    
    if ([self.mCaptureSession canAddOutput:self.mCaptureAudioOutput]) {
        [self.mCaptureSession addOutput:self.mCaptureAudioOutput];
    }
    [self.mCaptureAudioOutput setSampleBufferDelegate:self queue:mCaptureQueue];
       
    NSString *audioFile = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"abc.aac"];
    [[NSFileManager defaultManager] removeItemAtPath:audioFile error:nil];
    [[NSFileManager defaultManager] createFileAtPath:audioFile contents:nil attributes:nil];
    audioFileHandle = [NSFileHandle fileHandleForWritingAtPath:audioFile];
    
    [self.mCaptureSession startRunning];
}

Converter的创建

创建一个Converter,也就是一个AAC Encoder,输入参数分别是源和目的的数据格式。在AAC编码的场景下,源格式就是采集到的PCM数据,目的格式就是AAC。

extern OSStatus  
AudioConverterNew(      const AudioStreamBasicDescription*  inSourceFormat,  
                        const AudioStreamBasicDescription*  inDestinationFormat,  
                        AudioConverterRef*                  outAudioConverter)      __OSX_AVAILABLE_STARTING(__MAC_10_1,__IPHONE_2_0);
AudioStreamBasicDescription inAudioStreamBasicDescription;  
  
FillOutASBDForLPCM()  
inAudioStreamBasicDescription.mFormatID = kAudioFormatLinearPCM;  
inAudioStreamBasicDescription.mSampleRate = 44100;  
inAudioStreamBasicDescription.mBitsPerChannel = 16;  
inAudioStreamBasicDescription.mFramesPerPacket = 1;  
inAudioStreamBasicDescription.mBytesPerFrame = 2;  
inAudioStreamBasicDescription.mBytesPerPacket = inAudioStreamBasicDescription.mBytesPerFrame * inAudioStreamBasicDescription.mFramesPerPacket;  
inAudioStreamBasicDescription.mChannelsPerFrame = 1;  
inAudioStreamBasicDescription.mFormatFlags = kLinearPCMFormatFlagIsPacked | kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsNonInterleaved;  
inAudioStreamBasicDescription.mReserved = 0;  
  
AudioStreamBasicDescription outAudioStreamBasicDescription = {0}; 
// Always initialize the fields of a new audio stream basic description structure to zero, as shown here: ...  
  
outAudioStreamBasicDescription.mChannelsPerFrame = 1;  
outAudioStreamBasicDescription.mFormatID = kAudioFormatMPEG4AAC;  
UInt32 size = sizeof(outAudioStreamBasicDescription);  
AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &size, &outAudioStreamBasicDescription);  
  
OSStatus status = AudioConverterNew(&inAudioStreamBasicDescription, &outAudioStreamBasicDescription, &_audioConverter);  
if(status != 0) 
{
    NSLog(@"setup converter failed: %d", (int)status);
}  

这样就创建了AAC编码器,默认情况下,Apple会创建一个硬件编码器,如果硬件不可用,会创建软件编码器。硬件AAC编码器的编码时延很高,需要buffer大约2秒的数据才会开始编码。而软件编码器的编码时延就是正常的,只要喂给1024个样点,就会开始编码。


指定使用软件编码器

如何使用指定的软件编码器。

- (AudioClassDescription *)getAudioClassDescriptionWithType:(UInt32)type  
                                           fromManufacturer:(UInt32)manufacturer  
{  
    static AudioClassDescription desc;  
      
    UInt32 encoderSpecifier = type;  
    OSStatus st;  
      
    UInt32 size;  
    st = AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders,  
                                    sizeof(encoderSpecifier),  
                                    &encoderSpecifier,  
                                    &size);  
    if (st) {  
        NSLog(@"error getting audio format propery info: %d", (int)(st));  
        return nil;  
    }  
      
    unsigned int count = size / sizeof(AudioClassDescription);  
    AudioClassDescription descriptions[count];  
    st = AudioFormatGetProperty(kAudioFormatProperty_Encoders,  
                                sizeof(encoderSpecifier),  
                                &encoderSpecifier,  
                                &size,  
                                descriptions);  
    if (st) {  
        NSLog(@"error getting audio format propery: %d", (int)(st));  
        return nil;  
    }  
      
    for (unsigned int i = 0; i < count; i++) {  
        if ((type == descriptions[i].mSubType) &&  
            (manufacturer == descriptions[i].mManufacturer)) {  
            memcpy(&desc, &(descriptions[i]), sizeof(desc));  
            return &desc;  
        }  
    }  
      
    return nil;  
}  

AudioClassDescription *desc = [self getAudioClassDescriptionWithType:kAudioFormatMPEG4AAC   fromManufacturer:kAppleSoftwareAudioCodecManufacturer];  
OSStatus status = AudioConverterNewSpecific(&inAudioStreamBasicDescription, &outAudioStreamBasicDescription, 1, desc, &_audioConverter); 

设置编码码率参数

UInt32 ulBitRate = 64000;  
UInt32 ulSize = sizeof(ulBitRate);  
status = AudioConverterSetProperty(_audioConverter, kAudioConverterEncodeBitRate, ulSize, &ulBitRate); 

AAC并不是随便的码率都可以支持。比如如果PCM采样率是44100KHz,那么码率可以设置64000bps,如果是16K,可以设置为32000bps。


获取编码器最大输出

UInt32 value = 0;  
size = sizeof(value);  
AudioConverterGetProperty(_audioConverter, kAudioConverterPropertyMaximumOutputPacketSize, &size, &value);

开始编码

获取出来的Value表示编码器最大输出的包大小。
然后调用AudioConverterFillCOmplexBuffer进行编码。

AudioBufferList outAudioBufferList = {0};  
outAudioBufferList.mNumberBuffers = 1;  
outAudioBufferList.mBuffers[0].mNumberChannels = 1;  
outAudioBufferList.mBuffers[0].mDataByteSize = value;//value是上面查询到的值  
outAudioBufferList.mBuffers[0].mData = new int8[value];  
          
UInt32 ioOutputDataPacketSize = 1;  
status = AudioConverterFillComplexBuffer(_audioConverter, inInputDataProc, (__bridge voidvoid *)(self), &ioOutputDataPacketSize, &outAudioBufferList, NULL);  

编码接口中,inInputDataProc是一个输入数据的回调函数。用来喂PCM数据给Converter,ioOutputDataPacketSize为1表示编码产生1帧数据即返回。outAudioBufferList用来存放编码后的数据。

inInputDataProc中的处理如下:

static OSStatus inInputDataProc(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, voidvoid *inUserData)  
{  
    AACEncoder *encoder = (__bridge AACEncoder *)(inUserData);  
    UInt32 requestedPackets = *ioNumberDataPackets;  
    uint8_t *buffer;  
    uint32_t bufferLength = requestedPackets * 2;  
    uint32_t bufferRead;  
    bufferRead = [encoder.pcmPool readBuffer:&buffer withLength:bufferLength];  
    if (bufferRead == 0) {  
        *ioNumberDataPackets = 0;  
        return -1;  
    }  
  
    ioData->mBuffers[0].mData = buffer;  
    ioData->mBuffers[0].mDataByteSize = bufferRead;  
    ioData->mNumberBuffers = 1;  
    ioData->mBuffers[0].mNumberChannels = 1;  
      
    *ioNumberDataPackets = bufferRead >> 1;  
    return noErr;  
}  

添加ADTS头

AAC音频格式有ADIF和ADTS

  • ADIFAudio Data Interchange Format 音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。
  • ADTSAudio Data Transport Stream 音频数据传输流。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始。它的特征类似于mp3数据流格式。

AudioConverterFillComplexBuffer返回的是AAC原始码流,需要在AAC每帧添加ADTS头,调用adtsDataForPacketLength方法生成,最后把数据写入audioFileHandle的文件。

对于TS文件来说,每个AAC数据需要增加一个adts头,adts头是一个7bit的数据,通过adts可以得知AAC数据的编码参数,方便解码器进行解码。adts头的计算方法如下:

- (NSData*) adtsDataForPacketLength:(NSUInteger)packetLength 
{  
    int adtsLength = 7;  
    charchar *packet = (charchar *)malloc(sizeof(char) * adtsLength);  
    // Variables Recycled by addADTStoPacket  
    int profile = 2;  //AAC LC  
    //39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;  
    int freqIdx = 8;  //16KHz  
    int chanCfg = 1;  //MPEG-4 Audio Channel Configuration. 1 Channel front-center  
    NSUInteger fullLength = adtsLength + packetLength;  
    // fill in ADTS data  
    packet[0] = (char)0xFF; // 11111111     = syncword  
    packet[1] = (char)0xF9; // 1111 1 00 1  = syncword MPEG-2 Layer CRC  
    packet[2] = (char)(((profile-1)<<6) + (freqIdx<<2) +(chanCfg>>2));  
    packet[3] = (char)(((chanCfg&3)<<6) + (fullLength>>11));  
    packet[4] = (char)((fullLength&0x7FF) >> 3);  
    packet[5] = (char)(((fullLength&7)<<5) + 0x1F);  
    packet[6] = (char)0xFC;  
    NSData *data = [NSData dataWithBytesNoCopy:packet length:adtsLength freeWhenDone:YES];  
    return data;  
}  

后记

未完,待续~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容