iOS音视频开发-音频硬编码(AudioToolbox-PCM To AAC)

之前几篇文章记录了视频的软、硬编码过程,接下来将记录下音频的软、硬编码过程,学习、工作之余,以免忘记。
视频编码地址:
iOS音视频开发-视频会话捕捉
iOS音视频开发-视频硬编码(H264)
iOS音视频开发-视频软编码(x264编码H.264文件)
iOS音视频开发-视频软编码(FFmpeg+x264编码H.264文件)


PCM数据

PCM(Pulse Code Modulation)也被称为脉冲编码调制。PCM音频数据是未经压缩的音频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准的数字音频数据。
移动端对音频的实时采集编码传输,一般为将采集的音频数据设置为PCM格式数据,然后将PCM编码为AAC格式数据,以便后续传输。
PCM的数据格式,这里有篇文章介绍的很好,后续代码中的采样率、声道等均参考此文章设置。
PCM数据格式文章地址:点这里

ADTS

ADTS全称是(Audio Data Transport Stream),是AAC的一种十分常见的传输格式。
将PCM数据编码为AAC的时候需要将每帧的AAC数据添加ADTS header,否则将无法解码播放。
ADTS数据格式分为两部分:
固定头部:adts_fixed_header
可变头部:adts_variable_header
详见Wiki,地址在文章末尾。

主要代码

1、音频捕获代码

#import "BBAudioCapture.h"
#import <AVFoundation/AVFoundation.h>
#import "BBAudioConfig.h"
#import "BBAudioHardEncoder.h"
#import "BBAudioHardEncoder.h"

@interface BBAudioCapture ()
{
     AudioComponentInstance _outInstance;
}
@property (nonatomic, assign) AudioComponent        component;
@property (nonatomic, strong) AVAudioSession        *session;
@property (nonatomic, strong) BBAudioHardEncoder    *encoder;
@property (nonatomic, strong) NSFileHandle          *handle;
@end

@implementation BBAudioCapture

#pragma mark -- 对象销毁方法
- (void)dealloc{
    AudioComponentInstanceDispose(_outInstance);
}

#pragma mark -- 对外API(控制是否捕捉音频数据)
- (void)startRunning{
    AudioOutputUnitStart(_outInstance);
}

-(void)stopRunning{
    AudioOutputUnitStop(_outInstance);
}

#pragma mark -- 对外API(设置捕获音频数据配置项)
- (void)setConfig:(BBAudioConfig *)config{
    _config = config;
    [self private_setupAudioSession];
}

#pragma mark -- 私有API(初始化音频会话)
- (void)private_setupAudioSession{
    
    //0.初始化编码器
    self.encoder = [[BBAudioHardEncoder alloc] init];
    self.encoder.config = self.config;
    
    //1.获取音频会话实例
    self.session = [AVAudioSession sharedInstance];
    
    NSError *error = nil;
    [self.session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDefaultToSpeaker error:&error];
    
    if (error) {
        NSLog(@"AVAudioSession setupError");
        error = nil;
        return;
    }
    
    //2.激活会话
    [self.session setActive:YES error:&error];
    
    if (error) {
        NSLog(@"AVAudioSession setActiveError");
        error = nil;
        return;
    }
    
    //3.设置模式
    [self.session setMode:AVAudioSessionModeVideoRecording error:&error];
    
    if (error) {
        NSLog(@"AVAudioSession setModeError");
        error = nil;
        return;
    }
    
    //4.设置音频单元
    AudioComponentDescription acd = {
        .componentType = kAudioUnitType_Output,
        .componentSubType = kAudioUnitSubType_RemoteIO,
        .componentManufacturer = kAudioUnitManufacturer_Apple,
        .componentFlags = 0,
        .componentFlagsMask = 0,
    };
    
    //5.查找音频单元
    self.component = AudioComponentFindNext(NULL, &acd);
    
    //6.获取音频单元实例
    OSStatus status = AudioComponentInstanceNew(self.component, &_outInstance);
    
    if (status != noErr) {
        NSLog(@"AudioSource new AudioComponent error");
        status = noErr;
        return;
    }
    
    //7.设置音频单元属性-->可读写 0-->不可读写 1-->可读写
    UInt32 flagOne = 1;
    AudioUnitSetProperty(_outInstance, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &flagOne, sizeof(flagOne));
    
    //8.设置音频单元属性-->音频流
    AudioStreamBasicDescription asbd = {0};
    asbd.mSampleRate = self.config.sampleRate;//采样率
    asbd.mFormatID = kAudioFormatLinearPCM;//原始数据为PCM格式
    asbd.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
    asbd.mChannelsPerFrame = (UInt32)self.config.channels;//每帧的声道数量
    asbd.mFramesPerPacket = 1;//每个数据包多少帧
    asbd.mBitsPerChannel = 16;//16位
    asbd.mBytesPerFrame = asbd.mChannelsPerFrame * asbd.mBitsPerChannel / 8;//每帧多少字节 bytes -> bit / 8
    asbd.mBytesPerPacket = asbd.mFramesPerPacket * asbd.mBytesPerFrame;//每个包多少字节
    
    status = AudioUnitSetProperty(_outInstance, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &asbd, sizeof(asbd));
    
    if (status != noErr) {
        NSLog(@"AudioUnitSetProperty StreamFormat error");
        status = noErr;
        return;
    }
    
    //9.设置回调函数
    AURenderCallbackStruct cb;
    cb.inputProcRefCon = (__bridge void *)self;
    cb.inputProc = audioBufferCallBack;
    
    status = AudioUnitSetProperty(_outInstance, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 1, &cb, sizeof(cb));
    
    if(status != noErr){
        NSLog(@"AudioUnitSetProperty StreamFormat InputCallback error");
        status = noErr;
        return;
    }
    
    //10.初始化音频单元
    status = AudioUnitInitialize(_outInstance);
    
    if (status != noErr) {
        NSLog(@"AudioUnitInitialize error");
        status = noErr;
        return;
    }
    
    //11.设置优先采样率
    [self.session setPreferredSampleRate:self.config.sampleRate error:&error];
    
    if (error) {
        NSLog(@"AudioSource setPreferredSampleRate error");
        error = nil;
        return;
    }
    
    //12.aac文件夹地址
    NSString *audioPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.aac"];
    [[NSFileManager defaultManager] removeItemAtPath:audioPath error:nil];
    [[NSFileManager defaultManager] createFileAtPath:audioPath contents:nil attributes:nil];
    self.handle = [NSFileHandle fileHandleForWritingAtPath:audioPath];
    
}

#pragma mark -- 音频流回调函数
static OSStatus audioBufferCallBack(void *inRefCon,
                                    AudioUnitRenderActionFlags *ioActionFlags,
                                    const AudioTimeStamp *inTimeStamp,
                                    UInt32 inBusNumber,
                                    UInt32 inNumberFrames,
                                    AudioBufferList *ioData) {
    @autoreleasepool {
        BBAudioCapture *capture = (__bridge BBAudioCapture *)inRefCon;
        if(!capture) return -1;
        
        AudioBuffer buffer;
        buffer.mData = NULL;
        buffer.mDataByteSize = 0;
        buffer.mNumberChannels = 1;
        
        AudioBufferList buffers;
        buffers.mNumberBuffers = 1;
        buffers.mBuffers[0] = buffer;
        
        OSStatus status = AudioUnitRender(capture->_outInstance,
                                          ioActionFlags,
                                          inTimeStamp,
                                          inBusNumber,
                                          inNumberFrames,
                                          &buffers);
        
        if(status == noErr) {
            [capture.encoder encodeWithBufferList:buffers completianBlock:^(NSData *encodedData, NSError *error) {
                if (error) {
                    NSLog(@"error:%@",error);
                    return;
                }
                
                NSLog(@"write to file!");
                [capture.handle writeData:encodedData];
            }];
        }
        
        return status;
    }
}

@end

2、编码代码

#import "BBAudioHardEncoder.h"
#import "BBAudioConfig.h"

@interface BBAudioHardEncoder ()
@property (nonatomic, assign) AudioConverterRef converterRef;
@end
@implementation BBAudioHardEncoder

- (AudioConverterRef)converterRef{
    if (_converterRef == nil) {
        [self private_setupAudioConvert];
    }
    return _converterRef;
}

- (void)dealloc {
    AudioConverterDispose(_converterRef);
}

- (void)private_setupAudioConvert{
    
    //1.输入流
    AudioStreamBasicDescription inputFormat = {0};
    inputFormat.mSampleRate = self.config.sampleRate;//采样率
    inputFormat.mFormatID = kAudioFormatLinearPCM;//PCM采样
    inputFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
    inputFormat.mChannelsPerFrame = (UInt32)self.config.channels;//每帧声道数
    inputFormat.mFramesPerPacket = 1;//每包帧数
    inputFormat.mBitsPerChannel = 16;//每声道位数
    inputFormat.mBytesPerFrame = inputFormat.mBitsPerChannel / 8 * inputFormat.mChannelsPerFrame;//每帧的字节数
    inputFormat.mBytesPerPacket = inputFormat.mBytesPerFrame * inputFormat.mFramesPerPacket;//每包字节数
    
    //2.输出流
    AudioStreamBasicDescription outputFormat;
    //2.1初始清零
    memset(&outputFormat, 0, sizeof(outputFormat));
    //2.2音频流,在正常播放情况下的帧率。如果是压缩的格式,这个属性表示解压缩后的帧率。帧率不能为0。
    outputFormat.mSampleRate       = inputFormat.mSampleRate;
    //2.3AAC编码 kAudioFormatMPEG4AAC kAudioFormatMPEG4AAC_HE_V2
    outputFormat.mFormatID         = kAudioFormatMPEG4AAC;
    //2.4无损编码,0则无
    outputFormat.mFormatFlags      = kMPEG4Object_AAC_LC;
    //2.5每一个packet的音频数据大小。如果的动态大小设置为0。动态大小的格式需要用AudioStreamPacketDescription来确定每个packet的大小。
    outputFormat.mBytesPerPacket   = 0;
    //2.6每帧的声道数
    outputFormat.mChannelsPerFrame = (UInt32)self.config.channels;
    //2.7每个packet的帧数。如果是未压缩的音频数据,值是1。动态帧率格式,这个值是一个较大的固定数字,比如说AAC的1024。如果是动态大小帧数(比如Ogg格式)设置为0。
    outputFormat.mFramesPerPacket  = 1024;
    //2.8每帧的bytes数,每帧的大小。每一帧的起始点到下一帧的起始点。如果是压缩格式,设置为0 。
    outputFormat.mBytesPerFrame = 0;
    //2.9语音每采样点占用位数 压缩格式设置为0
    outputFormat.mBitsPerChannel = 0;
    //2.10字节对齐,填0.
    outputFormat.mReserved = 0;
    
    //3.编码器参数
    const OSType subtype = kAudioFormatMPEG4AAC;
    AudioClassDescription requestedCodecs[2] = {
        {
            kAudioEncoderComponentType,
            subtype,
            kAppleSoftwareAudioCodecManufacturer
        },
        {
            kAudioEncoderComponentType,
            subtype,
            kAppleHardwareAudioCodecManufacturer
        }
    };
    
    //4.编码器
    OSStatus result = AudioConverterNewSpecific(&inputFormat, &outputFormat, 2, requestedCodecs, &_converterRef);
    
    if (result == noErr) {
        NSLog(@"creat convert success!");
    }else{
        NSLog(@"creat convert error!");
        _converterRef = nil;
    }
    
}

- (void)encodeWithBufferList:(AudioBufferList)bufferList completianBlock:(void (^)(NSData *encodedData, NSError *error))completionBlock{
    if (!self.converterRef) {
        return;
    }
    int size = bufferList.mBuffers[0].mDataByteSize;
    
    if (size <= 0) {
        return;
    }
    
    char *aacBuf = malloc(size);
    
    //1.初始化一个输出缓冲列表
    AudioBufferList outBufferList;
    outBufferList.mNumberBuffers              = 1;
    outBufferList.mBuffers[0].mNumberChannels = bufferList.mBuffers[0].mNumberChannels;
    outBufferList.mBuffers[0].mDataByteSize   = bufferList.mBuffers[0].mDataByteSize; // 设置缓冲区大小
    outBufferList.mBuffers[0].mData           = aacBuf; // 设置AAC缓冲区
    UInt32 outputDataPacketSize               = 1;
    
    NSData *data = nil;
    NSError *error = nil;
    OSStatus status = AudioConverterFillComplexBuffer(_converterRef, inputDataProc, &bufferList, &outputDataPacketSize, &outBufferList, NULL);
    if (status == 0){
        NSData *rawAAC = [NSData dataWithBytes:outBufferList.mBuffers[0].mData length:outBufferList.mBuffers[0].mDataByteSize];
        NSData *adtsHeader = [self getADTSDataWithPacketLength:rawAAC.length];
        NSMutableData *fullData = [NSMutableData dataWithData:adtsHeader];
        [fullData appendData:rawAAC];
        data = fullData;
    }else{
        error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
        NSLog(@"音频编码失败");
        return;
    }
    
    if (completionBlock) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            completionBlock(data, error);
        });
    }
    free(aacBuf);
}

#pragma mark -- AudioCallBack
OSStatus inputDataProc(AudioConverterRef inConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData,AudioStreamPacketDescription **outDataPacketDescription, void *inUserData) {
    //填充PCM到缓冲区
    AudioBufferList bufferList = *(AudioBufferList*)inUserData;
    ioData->mBuffers[0].mNumberChannels = 1;
    ioData->mBuffers[0].mData           = bufferList.mBuffers[0].mData;
    ioData->mBuffers[0].mDataByteSize   = bufferList.mBuffers[0].mDataByteSize;
    ioData->mNumberBuffers              = 1;
    return noErr;
}

/**
 *  Add ADTS header at the beginning of each and every AAC packet.
 *  This is needed as MediaCodec encoder generates a packet of raw
 *  AAC data.
 *
 *  Note the packetLen must count in the ADTS header itself.
 *  See: http://wiki.multimedia.cx/index.php?title=ADTS
 *  Also: http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Channel_Configurations
 **/
- (NSData *)getADTSDataWithPacketLength:(NSInteger)packetLength {
    
    int adtsLength = 7;
    char *packet = malloc(sizeof(char) * adtsLength);
    // Variables Recycled by addADTStoPacket
    int profile = 2;  //AAC LC
    //39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
    int freqIdx = 4;  //44.1KHz
    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;
}
@end

以上代码为主要代码,配置代码无非就是采样率:44.1kHz,声道:双声道。
完整代码地址:https://github.com/ibabyblue/PCMHardEncodeToAAC
将编码的AAC数据写入本地文件,利用VLC播放器可以直接播放.aac格式文件,测试很方便。

写在最后,学习过程中非常感谢Jovins、iossigner分享的干活,感恩!
参考地址:
http://blog.csdn.net/ownwell/article/details/8114121/
https://wiki.multimedia.cx/index.php?title=ADTS
https://developer.apple.com/documentation/audiotoolbox/audio_converter_services?language=objc

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

推荐阅读更多精彩内容

  • 视频 视频实质:纯粹的视频(不包括音频)实质上就是一组帧图片,经过视频编码成为视频(video)文件再把音频(au...
    勇敢的_心_阅读 2,794评论 1 30
  • H264中的sps pps iOS仿微信小视频功能开发优化记录【如何快速的开发一个完整的iOS直播app】(原理篇...
    CharlyZheng阅读 1,299评论 0 2
  • 前言 本篇开始讲解在Android平台上进行的音频编辑开发,首先需要对音频相关概念有基础的认识。所以本篇要讲解以下...
    Ihesong阅读 7,557评论 2 18
  • 那一年我二十一岁,在我一生的黄金时代,我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云。后来我才知道,...
    静听晨钟暮鼓阅读 141评论 0 0
  • 杨帛翰阅读 126评论 0 0