iOS音頻編程之實時語音通信

title: iOS音頻編程之實時語音通信
date: 2016-07-14
tags: AAC Converter,Audio Queue,Audio Unit,MultipeerConnectivity
博客地址

iOS音頻編程之實時語音通信

需求:手機通過Mic采集PCM編碼的原始音頻數(shù)據(jù),將PCM轉(zhuǎn)換為AAC編碼格式,通過MultipeerConnectivity框架連接手機并發(fā)送AAC數(shù)據(jù)榆俺,在接收端使用Audio Queue播放收到的AAC音頻

音頻設(shè)置

對音頻以44.1KHZ的采樣率來采樣,以64000的比特率對PCM進行AAC轉(zhuǎn)碼

1)對AVAudioSession的設(shè)置

NSError *error;
self.session = [AVAudioSession sharedInstance];
[self.session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
handleError(error);
//route變化監(jiān)聽
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioSessionRouteChangeHandle:) name:AVAudioSessionRouteChangeNotification object:self.session];

[self.session setPreferredIOBufferDuration:0.005 error:&error];
handleError(error);
[self.session setPreferredSampleRate:kSmaple error:&error];
handleError(error);

//[self.session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
//handleError(error);

[self.session setActive:YES error:&error];
handleError(error);

-(void)audioSessionRouteChangeHandle:(NSNotification *)noti{
//    NSError *error;
//    [self.session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
//    handleError(error);
[self.session setActive:YES error:nil];
if (self.startRecord) {
    CheckError(AudioOutputUnitStart(_toneUnit), "couldnt start audio unit");
    }
}

音頻輸入輸出路徑改變會觸發(fā)audioSessionRouteChangeHandle,如果想一直讓音頻從手機的揚聲器輸出需要在每次Route改變時凑阶,把音頻輸出重定向到AVAudioSessionPortOverrideSpeaker,否則為手機聽筒輸出音頻;其他設(shè)置說明請參照iOS音頻編程之變聲處理的初始化部分

2)對Audio Unit的設(shè)置

AudioComponentDescription acd;
acd.componentType = kAudioUnitType_Output;
acd.componentSubType = kAudioUnitSubType_RemoteIO;
acd.componentFlags = 0;
acd.componentFlagsMask = 0;
acd.componentManufacturer = kAudioUnitManufacturer_Apple;
AudioComponent inputComponent = AudioComponentFindNext(NULL, &acd);
AudioComponentInstanceNew(inputComponent, &_toneUnit);


UInt32 enable = 1;
AudioUnitSetProperty(_toneUnit,
                     kAudioOutputUnitProperty_EnableIO,
                     kAudioUnitScope_Input,
                     kInputBus,
                     &enable,
                     sizeof(enable));


mAudioFormat.mSampleRate         = kSmaple;//采樣率
mAudioFormat.mFormatID           = kAudioFormatLinearPCM;//PCM采樣
mAudioFormat.mFormatFlags        = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
mAudioFormat.mFramesPerPacket    = 1;//每個數(shù)據(jù)包多少幀
mAudioFormat.mChannelsPerFrame   = 1;//1單聲道缘厢,2立體聲
mAudioFormat.mBitsPerChannel     = 16;//語音每采樣點占用位數(shù)
mAudioFormat.mBytesPerFrame      = mAudioFormat.mBitsPerChannel*mAudioFormat.mChannelsPerFrame/8;//每幀的bytes數(shù)
mAudioFormat.mBytesPerPacket     = mAudioFormat.mBytesPerFrame*mAudioFormat.mFramesPerPacket;//每個數(shù)據(jù)包的bytes總數(shù)倦卖,每幀的bytes數(shù)*每個數(shù)據(jù)包的幀數(shù)
mAudioFormat.mReserved           = 0;

CheckError(AudioUnitSetProperty(_toneUnit,
                                kAudioUnitProperty_StreamFormat,
                                kAudioUnitScope_Output, kInputBus,
                                &mAudioFormat, sizeof(mAudioFormat)),
           "couldn't set the remote I/O unit's input client format");

CheckError(AudioUnitSetProperty(_toneUnit,
                                kAudioOutputUnitProperty_SetInputCallback,
                                kAudioUnitScope_Output,
                                kInputBus,
                                &_inputProc, sizeof(_inputProc)),
           "couldnt set remote i/o render callback for input");


CheckError(AudioUnitInitialize(_toneUnit),
           "couldn't initialize the remote I/O unit");

具體參數(shù)說明請參照iOS音頻編程之變聲處理

采集音頻數(shù)據(jù)的輸入回調(diào)

static OSStatus inputRenderTone(
                     void *inRefCon,
                     AudioUnitRenderActionFlags     *ioActionFlags,
                     const AudioTimeStamp       *inTimeStamp,
                     UInt32                         inBusNumber,
                     UInt32                         inNumberFrames,
                     AudioBufferList            *ioData)

{

VoiceConvertHandle *THIS=(__bridge VoiceConvertHandle*)inRefCon;

AudioBufferList bufferList;
bufferList.mNumberBuffers = 1;
bufferList.mBuffers[0].mData = NULL;
bufferList.mBuffers[0].mDataByteSize = 0;
OSStatus status = AudioUnitRender(THIS->_toneUnit,
                                  ioActionFlags,
                                  inTimeStamp,
                                  kInputBus,
                                  inNumberFrames,
                                  &bufferList);

NSInteger lastTimeRear = recordStruct.rear;
for (int i = 0; i < inNumberFrames; i++) {
    SInt16 data = ((SInt16 *)bufferList.mBuffers[0].mData)[i];
    recordStruct.recordArr[recordStruct.rear] = data;
    recordStruct.rear = (recordStruct.rear+1)%kRecordDataLen;
    }
if ((lastTimeRear/1024 + 1) == (recordStruct.rear/1024)) {
     pthread_cond_signal(&recordCond);
    }
return status;
}

采用循環(huán)隊列存儲原始的音頻數(shù)據(jù),每1024點的PCM數(shù)據(jù)谎倔,讓Converter轉(zhuǎn)換為AAC編碼,所以當收集了1024點PCM后鸟辅,喚醒Converter線程氛什。

3)音頻轉(zhuǎn)碼

初始化

AudioStreamBasicDescription sourceDes = mAudioFormat;
AudioStreamBasicDescription targetDes;
memset(&targetDes, 0, sizeof(targetDes));
targetDes.mFormatID = kAudioFormatMPEG4AAC;
targetDes.mSampleRate = kSmaple;
targetDes.mChannelsPerFrame = sourceDes.mChannelsPerFrame;
UInt32 size = sizeof(targetDes);
CheckError(AudioFormatGetProperty(kAudioFormatProperty_FormatInfo,
                                  0, NULL, &size, &targetDes),
           "couldnt create target data format");


//選擇軟件編碼
AudioClassDescription audioClassDes;
CheckError(AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders,
                                      sizeof(targetDes.mFormatID),
                                      &targetDes.mFormatID,
                                      &size), "cant get kAudioFormatProperty_Encoders");
UInt32 numEncoders = size/sizeof(AudioClassDescription);
AudioClassDescription audioClassArr[numEncoders];
CheckError(AudioFormatGetProperty(kAudioFormatProperty_Encoders,
                                  sizeof(targetDes.mFormatID),
                                  &targetDes.mFormatID,
                                  &size,
                                  audioClassArr),
           "wrirte audioClassArr fail");
for (int i = 0; i < numEncoders; i++) {
    if (audioClassArr[i].mSubType == kAudioFormatMPEG4AAC
        && audioClassArr[i].mManufacturer == kAppleSoftwareAudioCodecManufacturer) {
        memcpy(&audioClassDes, &audioClassArr[i], sizeof(AudioClassDescription));
        break;
    }
}

CheckError(AudioConverterNewSpecific(&sourceDes, &targetDes, 1,
                                     &audioClassDes, &_encodeConvertRef),
           "cant new convertRef");

size = sizeof(sourceDes);
CheckError(AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCurrentInputStreamDescription, &size, &sourceDes), "cant get kAudioConverterCurrentInputStreamDescription");

size = sizeof(targetDes);
CheckError(AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCurrentOutputStreamDescription, &size, &targetDes), "cant get kAudioConverterCurrentOutputStreamDescription");

UInt32 bitRate = 64000;
size = sizeof(bitRate);
CheckError(AudioConverterSetProperty(_encodeConvertRef,
                                     kAudioConverterEncodeBitRate,
                                     size, &bitRate),
           "cant set covert property bit rate");
[self performSelectorInBackground:@selector(convertPCMToAAC) withObject:nil];

主要是設(shè)置編碼器的輸入音頻格式(PCM),輸出音頻格式(AAC),選擇軟件編碼器(默認使用硬件編碼器),設(shè)置編碼器的比特率

AAC編碼

-(void)convertPCMToAAC{
UInt32 maxPacketSize = 0;
UInt32 size = sizeof(maxPacketSize);
CheckError(AudioConverterGetProperty(_encodeConvertRef,
                                     kAudioConverterPropertyMaximumOutputPacketSize,
                                     &size,
                                     &maxPacketSize),
           "cant get max size of packet");

AudioBufferList *bufferList = malloc(sizeof(AudioBufferList));
bufferList->mNumberBuffers = 1;
bufferList->mBuffers[0].mNumberChannels = 1;
bufferList->mBuffers[0].mData = malloc(maxPacketSize);
bufferList->mBuffers[0].mDataByteSize = maxPacketSize;

for (; ; ) {
    @autoreleasepool {
        
    
    pthread_mutex_lock(&recordLock);
    while (ABS(recordStruct.rear - recordStruct.front) < 1024 ) {
        pthread_cond_wait(&recordCond, &recordLock);
    }
    pthread_mutex_unlock(&recordLock);
    
    SInt16 *readyData = (SInt16 *)calloc(1024, sizeof(SInt16));
    memcpy(readyData, &recordStruct.recordArr[recordStruct.front], 1024*sizeof(SInt16));
    recordStruct.front = (recordStruct.front+1024)%kRecordDataLen;
    UInt32 packetSize = 1;
    AudioStreamPacketDescription *outputPacketDescriptions = malloc(sizeof(AudioStreamPacketDescription)*packetSize);
    bufferList->mBuffers[0].mDataByteSize = maxPacketSize;
    CheckError(AudioConverterFillComplexBuffer(_encodeConvertRef,
                                               encodeConverterComplexInputDataProc,
                                               readyData,
                                               &packetSize,
                                               bufferList,
                                               outputPacketDescriptions),
               "cant set AudioConverterFillComplexBuffer");
    free(outputPacketDescriptions);
    free(readyData);

    NSMutableData *fullData = [NSMutableData dataWithBytes:bufferList->mBuffers[0].mData length:bufferList->mBuffers[0].mDataByteSize];
    
    if ([self.delegate respondsToSelector:@selector(covertedData:)]) {
        [self.delegate covertedData:[fullData copy]];
    }
    }
}

新建的bufferList是用來存放每次轉(zhuǎn)碼后的AAC音頻數(shù)據(jù).for循環(huán)中等待音頻輸入回調(diào)存滿1024個PCM數(shù)組并喚醒它。outputPacketDescriptions數(shù)組是每次轉(zhuǎn)換的AAC編碼后各個包的描述,但這里每次只轉(zhuǎn)換一包數(shù)據(jù)(由傳入的packetSize決定)匪凉。調(diào)用AudioConverterFillComplexBuffer觸發(fā)轉(zhuǎn)碼枪眉,他的第二個參數(shù)是填充原始音頻數(shù)據(jù)的回調(diào)。轉(zhuǎn)碼完成后再层,會將轉(zhuǎn)碼的數(shù)據(jù)存放在它的第五個參數(shù)中(bufferList).轉(zhuǎn)換完成的AAC就可以發(fā)送給另外一臺手機了贸铜。

填充原始數(shù)據(jù)回調(diào)

OSStatus encodeConverterComplexInputDataProc(AudioConverterRef inAudioConverter,
                                         UInt32 *ioNumberDataPackets,
                                         AudioBufferList *ioData,
                                         AudioStreamPacketDescription **outDataPacketDescription,
                                         void *inUserData)
{
    ioData->mBuffers[0].mData = inUserData;
    ioData->mBuffers[0].mNumberChannels = 1;
    ioData->mBuffers[0].mDataByteSize = 1024*2;
    *ioNumberDataPackets = 1024;
    return 0;
}

4)Audio Queue播放AAC音頻數(shù)據(jù)

Audio Queue基礎(chǔ)知識

音頻數(shù)據(jù)以一個個AudioQueueBuffer的形式存在與音頻隊列中堡纬,Audio Queue使用它提供的音頻數(shù)據(jù)來播放,某一個AudioQueueBuffer使用完畢后蒿秦,會調(diào)用Audio Queue的回調(diào)烤镐,要求用戶再在這個AudioQueueBuffer填入數(shù)據(jù),并使它加入Audio Queue中棍鳖,如此循環(huán)职车,達到不間斷播放音頻數(shù)據(jù)的效果。

Audio Queue初始化

CheckError(AudioQueueNewOutput(&targetDes,
                               fillBufCallback,
                               (__bridge void *)self,
                               NULL,
                               NULL,
                               0,
                               &(_playQueue)),
           "cant new audio queue");
CheckError( AudioQueueSetParameter(_playQueue,
                                   kAudioQueueParam_Volume, 1.0),
           "cant set audio queue gain");

for (int i = 0; i < 3; i++) {
    AudioQueueBufferRef buffer;
    CheckError(AudioQueueAllocateBuffer(_playQueue, 1024, &buffer), "cant alloc buff");
    BNRAudioQueueBuffer *buffObj = [[BNRAudioQueueBuffer alloc] init];
    buffObj.buffer = buffer;
    [_buffers addObject:buffObj];
    [_reusableBuffers addObject:buffObj];
}
[self performSelectorInBackground:@selector(playData) withObject:nil];

Audio Queue播放音頻數(shù)據(jù)

-(void)playData{
    for (; ; ) {
    @autoreleasepool {
        
    NSMutableData *data = [[NSMutableData alloc] init];
    pthread_mutex_lock(&playLock);
    if (self.aacArry.count%8 != 0 || self.aacArry.count == 0) {
        pthread_cond_wait(&playCond, &playLock);
    }
    AudioStreamPacketDescription *paks = calloc(sizeof(AudioStreamPacketDescription), 8);
    for (int i = 0; i < 8 ; i++) {//8包AAC數(shù)據(jù)組成放入一個AudioQueueBuffer的數(shù)據(jù)包
        BNRAudioData *audio = [self.aacArry firstObject];
        [data appendData:audio.data];
        paks[i].mStartOffset = audio.packetDescription.mStartOffset;
        paks[i].mDataByteSize = audio.packetDescription.mDataByteSize;
        [self.aacArry removeObjectAtIndex:0];
    }
    pthread_mutex_unlock(&playLock);
    
    pthread_mutex_lock(&buffLock);
    if (_reusableBuffers.count == 0) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            AudioQueueStart(_playQueue, nil);
        });
        pthread_cond_wait(&buffcond, &buffLock);
       
    }
    BNRAudioQueueBuffer *bufferObj = [_reusableBuffers firstObject];
    [_reusableBuffers removeObject:bufferObj];
    pthread_mutex_unlock(&buffLock);
    
    memcpy(bufferObj.buffer->mAudioData,[data bytes] , [data length]);
    bufferObj.buffer->mAudioDataByteSize = (UInt32)[data length];
    CheckError(AudioQueueEnqueueBuffer(_playQueue, bufferObj.buffer, 8, paks), "cant enqueue");
    free(paks);

    }
    }
}

static void fillBufCallback(void *inUserData,
                       AudioQueueRef inAQ,
                       AudioQueueBufferRef buffer){
VoiceConvertHandle *THIS=(__bridge VoiceConvertHandle*)inUserData;

for (int i = 0; i < THIS->_buffers.count; ++i) {
    if (buffer == [THIS->_buffers[i] buffer]) {
        pthread_mutex_lock(&buffLock);
        [THIS->_reusableBuffers addObject:THIS->_buffers[i]];
        pthread_mutex_unlock(&buffLock);
        pthread_cond_signal(&buffcond);
        break;
    }
    }   
}

playData中等待收到的aacArry數(shù)據(jù)鹊杖,這里要注意:每1024點PCM轉(zhuǎn)換成的一包AAC數(shù)據(jù)加入到AudioQueueBuffer中,不足以使Audio Queue播放音頻扛芽,所以這里使用8包AAC數(shù)據(jù)放到一個AudioQueueBuffer骂蓖。fillBufCallback是Audio Queue播放完一個AudioQueueBuffer調(diào)用的回調(diào)函數(shù),在這里面通知playData可以往使用完的AudioQueueBufferRef填數(shù)據(jù)了川尖,填完后登下,用AudioQueueEnqueueBuffer將它加入Audio Queue中,這個三個AudioQueueBufferRef不斷重用叮喳。

實時語音通信處理

原來是想用藍牙來傳送數(shù)據(jù)的被芳,但是自己寫的藍牙傳送數(shù)據(jù)機制的速度跟不上轉(zhuǎn)換的AAC數(shù)據(jù)。使用MultipeerConnectivity框架既可使用藍牙也可以使用WIFI來通信馍悟,底層自動選擇畔濒。當把兩個手機的WIFI都關(guān)掉時,他們使用藍牙來傳送數(shù)據(jù)锣咒,在剛剛建立通話時侵状,能聽到傳送的語音,之后就聽不到了毅整,使用wifi傳輸數(shù)據(jù)時不會出現(xiàn)這種情況趣兄。

  1. MultipeerConnectivity基礎(chǔ)知識

MCNearbyServiceAdvertiser發(fā)送廣播,并接收MCNearbyServiceBrowser端的邀請,MCSession發(fā)送接收數(shù)據(jù)悼嫉、管理連接狀態(tài)艇潭。建立連接和通信的流程是,MCNearbyServiceAdvertiser廣播服務(wù)戏蔑,MCNearbyServiceBrowser搜到這個服務(wù)后蹋凝,要求把這個服務(wù)所對用的MCPeerID加入到它自己(MCNearbyServiceBrowser端)的MCSession中,MCNearbyServiceAdvertiser收到這個邀請辛臊,并同意仙粱,同時也將MCNearbyServiceBrowser端對應(yīng)的MCPeerID加入到了它自己(MCNearbyServiceAdvertiser)的MCSession中.
之后雙方可以使用各自的MCSession發(fā)送接收數(shù)據(jù)翰铡。

2)各端發(fā)送本身轉(zhuǎn)碼的AAC數(shù)據(jù)俊扭,并接收對方發(fā)送的AAC數(shù)據(jù)提供給Auduio queue播放

源碼下載地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市笙隙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌隔心,老刑警劉巖白群,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異硬霍,居然都是意外死亡帜慢,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門唯卖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粱玲,“玉大人,你說我怎么就攤上這事拜轨〕榧酰” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵橄碾,是天一觀的道長卵沉。 經(jīng)常有香客問我,道長法牲,這世上最難降的妖魔是什么史汗? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮拒垃,結(jié)果婚禮上停撞,老公的妹妹穿的比我還像新娘。我一直安慰自己悼瓮,他們只是感情好怜森,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著谤牡,像睡著了一般副硅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上翅萤,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天恐疲,我揣著相機與錄音,去河邊找鬼套么。 笑死培己,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的胚泌。 我是一名探鬼主播省咨,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼玷室!你這毒婦竟也來了零蓉?” 一聲冷哼從身側(cè)響起笤受,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎敌蜂,沒想到半個月后箩兽,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡章喉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年汗贫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秸脱。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡落包,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出摊唇,到底是詐尸還是另有隱情妥色,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布遏片,位于F島的核電站,受9級特大地震影響撮竿,放射性物質(zhì)發(fā)生泄漏吮便。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一幢踏、第九天 我趴在偏房一處隱蔽的房頂上張望髓需。 院中可真熱鬧,春花似錦房蝉、人聲如沸僚匆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咧擂。三九已至,卻和暖如春檀蹋,著一層夾襖步出監(jiān)牢的瞬間松申,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工俯逾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留贸桶,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓桌肴,卻偏偏與公主長得像皇筛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子坠七,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容