iOS语音对讲(一)实时采集PCM并编码AAC

本文介绍iOS实时语音双向对讲(语音通话)功能:
(一)实时采集PCM并编码AAC
(二)RTSP+RTP协议实时传输
(三)FFmpeg实时解码AAC并播放PCM

第一篇介绍使用<AVFoundation/AVFoundation.h>中的AVCaptureSession进行音频的实时采集,输出PCM数据;再使用<AudioToolbox/AudioToolbox.h>中的AudioConverterRef将采集到的PCM进行编码转换,输出AAC。


具体过程如下:

1.采集

初始化AVCaptureSession并设置相关配置

- (instancetype)initCaptureWithPreset:(CapturePreset)preset {
    if ([super init]) {
        [self initAVcaptureSession];
        _definePreset = preset;
    }
    return self;
}

- (void)initAVcaptureSession {
    //初始化AVCaptureSession
    _session = [[AVCaptureSession alloc] init];
    //开始配置
    [_session beginConfiguration];
    NSError *error;
    //获取音频设备对象
    self.audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    //初始化捕获输入对象
    self.audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.audioDevice error:&error];
    if (error) {
        NSLog(@"录音设备出错");
    }
    //添加音频输入对象到session
    if ([self.session canAddInput:self.audioInput]) {
        [self.session addInput:self.audioInput];
    }
    //初始化输出捕获对象
    self.audioOutput = [[AVCaptureAudioDataOutput alloc] init];
    //添加音频输出对象到session
    if ([self.session canAddOutput:self.audioOutput]) {
        [self.session addOutput:self.audioOutput];
    }
    //创建设置音频输出代理所需要的线程队列
    dispatch_queue_t audioQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    [self.audioOutput setSampleBufferDelegate:self queue:audioQueue];
    //提交配置
    [self.session commitConfiguration];
}

开始与结束采集

- (void)start {
    [self.session startRunning];
}

- (void)stop {
    [self.session stopRunning];
}

设置代理回调将PCM数据输出

@protocol PCMCaptureDelegate <NSObject>
- (void)audioWithSampleBuffer:(CMSampleBufferRef)sampleBuffer;
@end
//AVCaptureAudioDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    if (captureOutput == self.audioOutput) {
        if (self.delegate && [self.delegate respondsToSelector:@selector(audioWithSampleBuffer:)]) {
            [self.delegate audioWithSampleBuffer:sampleBuffer];
        }
    }
}

2.编码

创建转换器并设置相关属性

- (void)setUpConverter:(CMSampleBufferRef)sampleBuffer {
    //获取audioformat的描述信息
    CMAudioFormatDescriptionRef audioFormatDes =  (CMAudioFormatDescriptionRef)CMSampleBufferGetFormatDescription(sampleBuffer);
    //获取输入的asbd的信息
    AudioStreamBasicDescription inAudioStreamBasicDescription = *(CMAudioFormatDescriptionGetStreamBasicDescription(audioFormatDes));
    //开始构造输出的asbd
    AudioStreamBasicDescription outAudioStreamBasicDescription = {0};
    //对于压缩格式必须设置为0
    outAudioStreamBasicDescription.mBitsPerChannel = 0;
    outAudioStreamBasicDescription.mBytesPerFrame = 0;
    //设定声道数为1
    outAudioStreamBasicDescription.mChannelsPerFrame = 1;
    //设定采样率为16000
    outAudioStreamBasicDescription.mSampleRate = 16000;
    //设定输出音频的格式
    outAudioStreamBasicDescription.mFormatID = kAudioFormatMPEG4AAC;
    outAudioStreamBasicDescription.mFormatFlags = kMPEG4Object_AAC_LC;
    //填充输出的音频格式
    UInt32 size = sizeof(outAudioStreamBasicDescription);
    AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &size, &outAudioStreamBasicDescription);
    //选择aac的编码器(用来描述一个已经安装的编解码器)
    AudioClassDescription audioClassDes;
    //初始化为0
    memset(&audioClassDes, 0, sizeof(audioClassDes));
    //获取满足要求的aac编码器的总大小
    UInt32 countSize = 0;
    AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders, sizeof(outAudioStreamBasicDescription.mFormatID), &outAudioStreamBasicDescription.mFormatID, &countSize);
    //用来计算aac的编解码器的个数
    int cout = countSize/sizeof(audioClassDes);
    //创建一个包含有cout个数的编码器数组
    AudioClassDescription descriptions[cout];
    //将编码器数组信息写入到descriptions中
    AudioFormatGetProperty(kAudioFormatProperty_Encoders, sizeof(outAudioStreamBasicDescription.mFormatID), &outAudioStreamBasicDescription.mFormatID, &countSize, descriptions);
    for (int i = 0; i < cout; cout++) {
        AudioClassDescription temp = descriptions[i];
        if (temp.mManufacturer==kAppleSoftwareAudioCodecManufacturer//软编
            &&temp.mSubType==outAudioStreamBasicDescription.mFormatID) {
            audioClassDes = temp;
            break;
        }
    }
    //创建convertcontext用来保存converter的信息
    ConverterContext *context = malloc(sizeof(ConverterContext));
    self->convertContext = context;
    OSStatus result = AudioConverterNewSpecific(&inAudioStreamBasicDescription, &outAudioStreamBasicDescription, 1, &audioClassDes, &(context->converter));
    if (result == noErr) {
        //创建编解码器成功
        AudioConverterRef converter = context->converter;
        //设置编码器属性
        UInt32 temp = kAudioConverterQuality_High;
        AudioConverterSetProperty(converter, kAudioConverterCodecQuality, sizeof(temp), &temp);
        //设置比特率
        UInt32 bitRate = 32000;
        result = AudioConverterSetProperty(converter, kAudioConverterEncodeBitRate, sizeof(bitRate), &bitRate);
        if (result != noErr) {
            NSLog(@"设置比特率失败");
        }
    }else{
        //创建编解码器失败
        free(context);
        context = NULL;
        NSLog(@"创建编解码器失败");
    }
}

编码samplebuffer数据

//编码samplebuffer数据
- (void)encodeSmapleBuffer:(CMSampleBufferRef)sampleBuffer {
    if (!self->convertContext) {
        [self setUpConverter:sampleBuffer];
    }
    ConverterContext *cxt = self->convertContext;
    if (cxt && cxt->converter) {
        //从samplebuffer中提取数据
        CFRetain(sampleBuffer);
        dispatch_async(encodeQueue, ^{
            //从samplebuffer中获取blockbuffer
            CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
            size_t pcmLength = 0;
            char *pcmData = NULL;
            //获取blockbuffer中的pcm数据的指针和长度
            OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &pcmLength, &pcmData);
            if (status != noErr) {
                NSLog(@"从block中获取pcm数据失败");
                CFRelease(sampleBuffer);
                return;
            } else {
                //在堆区分配内存用来保存编码后的aac数据
                char *outputBuffer = malloc(pcmLength);
                memset(outputBuffer, 0, pcmLength);
                UInt32 packetSize = 1;
                AudioStreamPacketDescription *outputPacketDes = (AudioStreamPacketDescription *)malloc(sizeof(AudioStreamPacketDescription) *packetSize);
                //使用fillcomplexinputparm来保存pcm数据
                FillComplexInputParm userParam;
                userParam.source = pcmData;
                userParam.sourceSize = (UInt32)pcmLength;
                userParam.channelCount = 1;
                userParam.packetDescription = NULL;
                //在堆区创建audiobufferlist
                AudioBufferList outputBufferList;
                outputBufferList.mNumberBuffers = 1;
                outputBufferList.mBuffers[0].mData = outputBuffer;
                outputBufferList.mBuffers[0].mDataByteSize = (unsigned int)pcmLength;
                outputBufferList.mBuffers[0].mNumberChannels = 1;
                //编码
                status = AudioConverterFillComplexBuffer(self->convertContext->converter, audioConverterComplexInputDataProc, &userParam, &packetSize, &outputBufferList, outputPacketDes);
                free(outputPacketDes);
                outputPacketDes = NULL;
                if (status == noErr) {
//                    NSLog(@"编码成功");
                    //获取原始的aac数据
                    NSData *rawAAC = [NSData dataWithBytes:outputBufferList.mBuffers[0].mData length:outputBufferList.mBuffers[0].mDataByteSize];
                    free(outputBuffer);
                    outputBuffer = NULL;
                    //设置adts头
                    int headerLength = 0;
                    char *packetHeader = newAdtsDataForPacketLength((int)rawAAC.length, &headerLength);
                    NSData *adtsHeader = [NSData dataWithBytes:packetHeader length:headerLength];
                    free(packetHeader);
                    packetHeader = NULL;
                    NSMutableData *fullData = [NSMutableData dataWithData:adtsHeader];
                    [fullData appendData:rawAAC];
                    //设置私有头
                    char *privateHeader = newPrivate((int)fullData.length);
                    NSData *privateHeaderData = [NSData dataWithBytes:privateHeader length:24];
                    free(privateHeader);
                    privateHeader = NULL;
                    NSMutableData *pFullData = [NSMutableData dataWithData:privateHeaderData];
                    [pFullData appendData:fullData];
                    //设置rtp头
                    char *rtpHeader = newRTPForAAC();
                    NSData *rtpHeaderData = [NSData dataWithBytes:rtpHeader length:12];
                    free(rtpHeader);
                    rtpHeader = NULL;
                    NSMutableData *fullData1 = [NSMutableData dataWithData:rtpHeaderData];
                    [fullData1 appendData:pFullData];
                    //设置rtsp interleaved frame头
                    char *rtspFrameHeader = newRTSPInterleavedFrame((int)fullData1.length);
                    NSData *rtspFrameHeaderData = [NSData dataWithBytes:rtspFrameHeader length:4];
                    free(rtspFrameHeader);
                    rtspFrameHeader = NULL;
                    NSMutableData *fullData2 = [NSMutableData dataWithData:rtspFrameHeaderData];
                    [fullData2 appendData:fullData1];
                    //发送数据
                    [self.delegate sendData:fullData2];
                    fullData2 = nil;
                    fullData1 = nil;
                    fullData = nil;
                    rawAAC = nil;
                }
                free(outputBuffer);
                CFRelease(sampleBuffer);
            }
        });
    }
}

其中AudioConverterFillComplexBuffer即是用于转换的函数,转换出来的AAC是raw data,需要添加固定字节(56bits)的ADTS头信息,用于描述音频的信息,便于解码器读取,关于ADTS的描述,可参考https://blog.csdn.net/jay100500/article/details/52955232,下面是添加ADTS的具体代码:

//给aac加上adts头, packetLength 为rawaac的长度
char *newAdtsDataForPacketLength(int packetLength, int *ioHeaderLen) {
    //adts头的长度为固定的7个字节
    int adtsLen = 7;
    //在堆区分配7个字节的内存
    char *packet = malloc(sizeof(char)*adtsLen);
    //选择AAC LC
    int profile = 2;
    //选择采样率对应的下标
    int freqIdx = 8;
    //选择声道数所对应的下标
    int chanCfg = 1;
    //获取adts头和raw aac的总长度
    NSUInteger fullLength = adtsLen+packetLength;
    //设置syncword
    packet[0] = 0xFF;
    packet[1] = 0xF1;
    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;
    *ioHeaderLen = adtsLen;
    return packet;
}
PS:这里作者除了添加ADTS头之外,还增加了私有头,所以不是标准的封装格式,可忽略

本文中使用RTSP+RTP协议将编码后的ADTS-AAC进行传输,所以还需要添加RTP头(96bits)以及RTSP Interleaved frame头(32bits)进行封装,均为固定字节,关于RTP的描述,可参考https://blog.csdn.net/qingkongyeyue/article/details/60590613,下面是RTP封装的具体代码:

//添加RTP头
char *newRTPForAAC() {
    //RTP头长度为固定的12个字节
    int rtpLen = 12;
    //在堆区分配12个字节的内存
    char *packet = malloc(sizeof(char)*rtpLen);
    //设置syncword
    packet[0] = 0x80;//V_P_X_CC
    packet[1] = 0x88;//M_PT
    //Sequence
    packet[2] = 0x00;
    packet[3] = 0xDA;
    //timestamp
    packet[4] = 0x00;
    packet[5] = 0x01;
    packet[6] = 0x98;
    packet[7] = 0xC0;
    //SSRC
    packet[8] = 0x00;
    packet[9] = 0x00;
    packet[10] = 0x00;
    packet[11] = 0x00;
    return packet;
}
PS:由于作者不是标准的封装格式,所以RTP头中某些值并没有进行校验,所以也不够标准,这里需要大家按照RTP规则进行封装

RTSP Interleaved frame头只有4字节,首字符为"$",下面是具体代码:

char *newRTSPInterleavedFrame(int packetLength) {
    //RTP头长度为固定的4个字节
    int rtpLen = 4;
    //在堆区分配4个字节的内存
    char *packet = malloc(sizeof(char)*rtpLen);
    //设置syncword
    packet[0] = 0x24;
    packet[1] = 0x00;
    NSString *str = [NSString stringWithFormat:@"%d",packetLength];
    long long l = [str longLongValue];
    NSString *s16 = [NSString stringWithFormat:@"%04llx",l];
    NSString *s16_1 = [s16 substringToIndex:2];
    NSString *s16_2 = [s16 substringFromIndex:2];
    unsigned long res1 = strtoul([s16_1 UTF8String],0,16);
    unsigned long res2 = strtoul([s16_2 UTF8String],0,16);
    packet[2] = res1;
    packet[3] = res2;
    return packet;
}

最后,fullData2即是最后封装后的数据,也就是说,数据前面被增加了一层又一层的各种头信息,此时就可以拿来进行RTSP传输了。


Demo地址:https://github.com/XuningZhai/TalkDemo
支持G711的Demo地址:https://github.com/XuningZhai/TalkDemo_G711_AAC

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

推荐阅读更多精彩内容