AudioToolBox音频编解码(八)

前言

AAC音频数据格式是一个很常见的音频压缩编码数据格式,经常会有这样的需求,通过麦克风采集到的PCM格式音频数据,编码为AAC格式音频数据,然后通过网络发给另一端,网络的另一端接收到AAC格式音频数据后,解码为PCM格式音频数据。本文将介绍用AudioToobox框架实现音频的编解码

AAC数据格式封装格式ADTS解析

原始音频编码后形成的裸的AAC数据是无法直接解码的,一般会在前面添加能够描述音频信息(如采样率,采样格式,声道数)的头部信息,常见的有ADTS封装格式,如下:
(ADTS头部)(压缩的AAC音频数据)
ADTS头部
它描述了音频的采样率,采样格式,声道数等信息;具体格式参考如下网站:
http://wiki.multimedia.cx/index.php?title=ADTS
http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Channel_Configurations
下面是由压缩的AAC数据长度/声道数,采样率为44.1kHz生成ADTS头部的代码:

- (NSData *)getADTSDataWithPacketLength:(NSInteger)packetLength channel:(int)channel
{
    
    int adtsLength = 7;
    char *packet = malloc(sizeof(char) * adtsLength);
    // Variables Recycled by addADTStoPacket
    int profile = 2;  //AAC LC      编码压缩级别
    int freqIdx = 4;  //44.1KHz     // 采样率
    int chanCfg = channel;          // 声道数
    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[1] = (char)0xF1; // 1111 0 00 1  = syncword MPEG-4 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;
}

下面是由一段ADTS数据解析出声道数,采样率的代码

// 解析ADTS 头部的采样率,声道数等信息
- (ADAudioFormat)getADTSInfo:(NSData *)adtsData
{
    const unsigned char buff[10];
    [adtsData getBytes:(void*)buff length:adtsData.length];
    
    unsigned long long adts = 0;
    const unsigned char *p = buff;
    adts |= *p ++; adts <<= 8;
    adts |= *p ++; adts <<= 8;
    adts |= *p ++; adts <<= 8;
    adts |= *p ++; adts <<= 8;
    adts |= *p ++; adts <<= 8;
    adts |= *p ++; adts <<= 8;
    adts |= *p ++;
    
    ADAudioFormat format;
    // 获取声道数
    format.channels = (adts >> 30) & 0x07;
    // 获取采样率
    format.samplerate = (adts >> 34) & 0x0f;
    return format;
}

AudioToolbox编码PCM音频数据为AAC音频数据

1、获取原始PCM音频数据
这里用AVCaptureSession实现。代码如下

// 1、初始化AVCaptureSession
self.captureSession = [[AVCaptureSession alloc] init];

// 2、获取音频设备对象
AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];

// 3、根据音频设备对象生成对应的音频输入对象,并将该音频输入对象添加到AVCaptureSession中
self.audioCaptureInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:nil];
if ([self.captureSession canAddInput:self.audioCaptureInput]) {
    NSLog(@"添加了输入");
    [self.captureSession addInput:self.audioCaptureInput];
}

// 4、创建音频数据输出对象并将该输出对象添加到AVCaptureSession中
self.audioCaptureOutput = [[AVCaptureAudioDataOutput alloc] init];
if ([self.captureSession canAddOutput:self.audioCaptureOutput]) {
    NSLog(@"添加了输出");
    [self.captureSession addOutput:self.audioCaptureOutput];
}

// 5、设置音频输出对象的回调
[self.audioCaptureOutput setSampleBufferDelegate:self queue:_audioQueue];

// 6、启动运行
[self.captureSession startRunning];

2、在回调中获取原始音频数据和数据格式

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    if (self.audioCaptureOutput == output) {    // 音频输出
    }
}

这里解释下CMSampleBufferRef,它是一个描述音视频数据对象的结构体,它既可以用于描述压缩的音/视频,也可以用于描述原始的音/视频;位于CoreMedia下的CMSampleBuffer.h头文件下,一般是AVCaptureOutput生成的对象
1、它包含了音/视频的数据格式
2、它包含了具体的音/视频数据(压缩或者未压缩的)
然后将CMSampleBufferRef中的原始音频数据的格式及具体的PCM音频数据提取出来

// 提取音频数据
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
CFRetain(blockBuffer);
size_t dataSize=0;
char *buffer = (char*)malloc(1024*10*4);
OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &dataSize, &buffer);
if (status != kCMBlockBufferNoErr) {
    NSLog(@"没有出错");
} else {
    NSLog(@"获取的数据大小 %ld",dataSize);
}

// 提取音频数据格式

// 初始化
AudioStreamBasicDescription inASBD = *CMAudioFormatDescriptionGetStreamBasicDescription(CMSampleBufferGetFormatDescription(sampleBuffer));

3、初始化编码器
首先定义压缩后的aac格式描述对象

AudioStreamBasicDescription destASBD = {0};
destASBD.mFormatID = kAudioFormatMPEG4AAC;      // 表示AAC编码
destASBD.mFormatFlags = kMPEG4Object_AAC_LC;    // aac 编码的 profile
destASBD.mSampleRate = sourceASBD.mSampleRate;  // 采样率不变,与原始PCM数据保持一致
destASBD.mChannelsPerFrame = sourceASBD.mChannelsPerFrame;  // 声道数不变,与原始PCM数据保持一致
destASBD.mFramesPerPacket = 1024;   // 对于aac的固定码率方式 值为1024
destASBD.mBitsPerChannel = 0;       // 填0就好
destASBD.mBytesPerFrame = 0;        // 填0就好
destASBD.mBytesPerPacket = 0;       // 填0就好
destASBD.mReserved = 0;

然后根据输入PCM音频数据数据格式和这里定义的压缩后的AAC编码的数据格式创建编码转换器对象AudioConverterRef
这里解释下AudioConverterRef
位于AudioToolbox下的AudioConvert.h中定义,它的作用如下:
1、编码
2、解码
3、采样率/声道数/采样格式的转换

- (id)initWithSourceASBD:(AudioStreamBasicDescription)sourceASBD destASBD:(AudioStreamBasicDescription)destASBD
{
    if (self = [super init]) {
        
        // 创建用于格式转化的缓冲区,避免重复创建内存
        _buffer = (uint8_t*)malloc(max_buffer_size);
        _bufferSize = max_buffer_size;
        memset(_buffer, 0, _bufferSize);
        _sourceASBD = sourceASBD;
        _destASBD = destASBD;
        
        // 编码器参数
        AudioClassDescription classspecific = [self classDesWithFormatPropertyId:kAudioFormatProperty_Encoders subType:kAudioFormatMPEG4AAC manufacturer:kAppleHardwareAudioCodecManufacturer];
        // 根据指定的格式参数创建格式转换器
        CheckStatusReturn(AudioConverterNewSpecific(&sourceASBD, &destASBD, 1, &classspecific, &_audioConverter),@"AudioConverterNewSpecific fail");
//        //也可以用下面这种方式
//        const OSType subtype = kAudioFormatMPEG4AAC;
//        AudioClassDescription classspecific[2] = {
//            {
//                kAudioEncoderComponentType,
//                subtype,
//                kAppleSoftwareAudioCodecManufacturer
//            },
//        };
//        CheckStatusReturn(AudioConverterNewSpecific(&sourceASBD, &destASBD, 2, classspecific, &_audioConverter),@"AudioConverterNewSpecific fail");
    }
    return self;
}

先看一下classDesWithFormatPropertyId方法,它定义了编码相关的参数,比如是使用硬编码还是软编码等等

/** 创建格式转换器时的参数
 */
- (AudioClassDescription)classDesWithFormatPropertyId:(AudioFormatPropertyID)formatPropertyId
                                              subType:(AudioFormatID)type
                                         manufacturer:(UInt32)manufacturer
{
    AudioClassDescription returnClassDes = {0};
    
    OSStatus status = noErr;
    // 获取指定的AudioFormatPropertyID的格式参数下inSpecifier类型为type的有多少个AudioClassDescription表示的属性
    UInt32 size;
    status = AudioFormatGetPropertyInfo(formatPropertyId, sizeof(type), &type, &size);
    if (status != noErr) {
        NSLog(@"AudioFormatGetPropertyInfo fail %d",status);
    }
    
    // 获取指定的AudioFormatPropertyID的格式参数下inSpecifier类型为type的指定数目的AudioClassDescription表示的属性
    UInt32 count = size / sizeof(AudioClassDescription);
    AudioClassDescription descriptions[count];
    status = AudioFormatGetProperty(formatPropertyId,
                                sizeof(type),
                                &type,
                                &size,
                                descriptions);
    if (status) {
        NSLog(@"error getting audio format propery: %d", (int)(status));
    }
    
    // 匹配指定的属性,然后拷贝过去
    for (unsigned int i = 0; i < count; i++) {
        if ((type == descriptions[i].mSubType) &&
            (manufacturer == descriptions[i].mManufacturer)) {
            memcpy(&returnClassDes, &(descriptions[i]), sizeof(returnClassDes));
        }
    }
    
    return returnClassDes;
}

然后使用AudioConverterNewSpecific()方法创建编码器
4、传入原始音频数据进行编码
对于第二步的回调函数提供的音频数据是CMSampleBufferRef类型的数据,我们可以将其中的音频数据提取出来组装成AudioUnitBuffList格式,如下

// 输入音频的数据格式
size_t size;
const AudioChannelLayout *layout =  CMAudioFormatDescriptionGetChannelLayout(CMSampleBufferGetFormatDescription(sampleBuffer), &size);
AudioBufferList inputBufferlist;
inputBufferlist.mNumberBuffers = 1;
inputBufferlist.mBuffers[0].mNumberChannels = AudioChannelLayoutTag_GetNumberOfChannels(layout->mChannelLayoutTag);
inputBufferlist.mBuffers[0].mData = malloc(dataSize);
inputBufferlist.mBuffers[0].mDataByteSize = (UInt32)dataSize;
memcpy(inputBufferlist.mBuffers[0].mData, buffer, dataSize);
// 将PCM数据编码为ADTS封装的AAC数据格式
- (BOOL)doEncodeBufferList:(AudioBufferList)fromBufferList toADTSData:(NSData**)todata
{
    if (!_audioConverter) {
        NSLog(@"转换器还没有创建");
        return NO;
    }
    int size = fromBufferList.mBuffers[0].mDataByteSize;
    
    if (size<= 0) {
        NSLog(@"fromBufferList 中没有数据");
        return NO;
    }
    
    // 对于编码数据来说 没有planner的概念
    UInt32  channel = fromBufferList.mBuffers[0].mNumberChannels;
    AudioBufferList outAudioBufferList = {0};
    outAudioBufferList.mNumberBuffers = 1;
    outAudioBufferList.mBuffers[0].mNumberChannels = channel;
    outAudioBufferList.mBuffers[0].mDataByteSize = _bufferSize;
    outAudioBufferList.mBuffers[0].mData = _buffer;
    UInt32 ioOutputDataPacketSize = 1;
    
    OSStatus status = AudioConverterFillComplexBuffer(_audioConverter,inInputDataProc, &fromBufferList, &ioOutputDataPacketSize,&outAudioBufferList, NULL);
    
    if (status == 0){
        NSData *rawAAC = [NSData dataWithBytes:outAudioBufferList.mBuffers[0].mData length:outAudioBufferList.mBuffers[0].mDataByteSize];
        NSData *adtsHeader = [self getADTSDataWithPacketLength:rawAAC.length channel:channel];
        NSMutableData *fullData = [NSMutableData dataWithData:adtsHeader];
        [fullData appendData:rawAAC];
        *todata = fullData;
    }else{
        NSLog(@"音频编码失败");
        return NO;
    }
    
    return YES;
}

请看NSData *adtsHeader = [self getADTSDataWithPacketLength:rawAAC.length channel:channel];这一行
,他的作用是调用前面介绍的ADTS头部生成函数生成7个字节的ADTS头部
最后将ADTS头部与原始的AAC裸数据合并组成ADTS封装格式的数据然后返回

以上就是音频编码为AAC并封装为ADTS格式的全过程,最后我们可以将最后一步获取到的adts格式数据直接写入到.aac后缀结尾的文件中,生成的文件可以直接用PC或者手机播放

AAC数据解码为PCM音频数据

前面我们知道,要解码AAC格式的压缩数据,必须要有头部信息才行,比如ADTS封装格式的AAC数据,接下来我们就简单点,直接解码前面第四步获取到的ADTS数据
1、首先创建解码器
解码器和编码器一样,都是由AudioConverterRef实现的,他们是个逆过程,也就是前面第3步中代码行的AudioConverterNewSpecific(&sourceASBD,&destASBD, 1, &classspecific, &_audioConverter))中的参数sourceASBD和destASBD调换一下即可
其它调用
2、AAC解码为PCM
解码之前首先得将ADTS头部信息剥离出来,并且解析出采样率,采样格式等等信息

Byte crcFlag;
    [fromData getBytes:&crcFlag range:NSMakeRange(1, 1)];
    // 先去掉头部
    NSData *realyData = nil;
    NSData *adtsData = nil;
    if (crcFlag & 0x08) {   // 说明ADTS头部占用7个字节
        realyData = [fromData subdataWithRange:NSMakeRange(7, fromData.length-7)];
        adtsData = [fromData subdataWithRange:NSMakeRange(0,7)];
    } else {                // 说明ADTS头部占用9个字节
        realyData = [fromData subdataWithRange:NSMakeRange(9, fromData.length-9)];
        adtsData = [fromData subdataWithRange:NSMakeRange(0,9)];
    }
    
    ADAudioFormat format = [self getADTSInfo:adtsData];
    
    AudioBufferList fromBufferlist;
    fromBufferlist.mNumberBuffers = 1;
    fromBufferlist.mBuffers[0].mNumberChannels = format.channels;
    fromBufferlist.mBuffers[0].mData = (void*)malloc(realyData.length);
    fromBufferlist.mBuffers[0].mDataByteSize = (UInt32)realyData.length;

其它代码与编码一模一样

参考Demo

Demo

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

推荐阅读更多精彩内容