本文介紹iOS實(shí)時(shí)語音雙向?qū)χv(語音通話)功能:
(一)實(shí)時(shí)采集PCM并編碼AAC
(二)RTSP+RTP協(xié)議實(shí)時(shí)傳輸
(三)FFmpeg實(shí)時(shí)解碼AAC并播放PCM
第三篇介紹使用
FFmpeg將通過網(wǎng)絡(luò)實(shí)時(shí)讀取到的AAC數(shù)據(jù)解碼為PCM并使用AudioQueueRef播放PCM。
關(guān)于FFmpeg的編譯及集成:FFmpeg for iOS 3.4 編譯與集成
具體過程如下:
1.解碼
初始化解碼器
- (BOOL)initAACDecoderWithSampleRate:(int)sampleRate channel:(int)channel bit:(int)bit {
av_register_all();
avformat_network_init();
self.aacCodec = avcodec_find_decoder(AV_CODEC_ID_AAC);
av_init_packet(&_aacPacket);
if (self.aacCodec != nil) {
self.aacCodecCtx = avcodec_alloc_context3(self.aacCodec);
// 初始化codecCtx
self.aacCodecCtx->codec_type = AVMEDIA_TYPE_AUDIO;
self.aacCodecCtx->sample_rate = sampleRate;
self.aacCodecCtx->channels = channel;
self.aacCodecCtx->bit_rate = bit;
self.aacCodecCtx->channel_layout = AV_CH_LAYOUT_STEREO;
// 打開codec
if (avcodec_open2(self.aacCodecCtx, self.aacCodec, NULL) >= 0) {
self.aacFrame = av_frame_alloc();
}
}
return (BOOL)self.aacFrame;
}
解碼AAC,block中返回解碼后的PCM
- (void)AACDecoderWithMediaData:(NSData *)mediaData sampleRate:(int)sampleRate completion:(void (^)(uint8_t *, size_t))completion {
_aacPacket.data = (uint8_t *)mediaData.bytes;
_aacPacket.size = (int)mediaData.length;
if (!self.aacCodecCtx) {
return;
}
if (&_aacPacket) {
avcodec_send_packet(self.aacCodecCtx, &_aacPacket);
int result = avcodec_receive_frame(self.aacCodecCtx, self.aacFrame);
//如果FFmpeg版本過舊,請(qǐng)使用avcodec_decode_audio4進(jìn)行解碼
/*int gotframe = 0;
int result = avcodec_decode_audio4(self.aacCodecCtx,
self.aacFrame,
&gotframe,
&_aacPacket);*/
if (result == 0) {
struct SwrContext *au_convert_ctx = swr_alloc();
au_convert_ctx = swr_alloc_set_opts(au_convert_ctx,
AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, sampleRate,
self.aacCodecCtx->channel_layout, self.aacCodecCtx->sample_fmt, self.aacCodecCtx->sample_rate,
0, NULL);
swr_init(au_convert_ctx);
int out_linesize;
int out_buffer_size = av_samples_get_buffer_size(&out_linesize, self.aacCodecCtx->channels,self.aacCodecCtx->frame_size,self.aacCodecCtx->sample_fmt, 1);
uint8_t *out_buffer = (uint8_t *)av_malloc(out_buffer_size);
// 轉(zhuǎn)換
swr_convert(au_convert_ctx, &out_buffer, out_linesize, (const uint8_t **)self.aacFrame->data , self.aacFrame->nb_samples);
swr_free(&au_convert_ctx);
au_convert_ctx = NULL;
if (completion) {
completion(out_buffer, out_linesize);
}
av_free(out_buffer);
}
}
}
釋放解碼器
- (void)releaseAACDecoder {
if(self.aacCodecCtx) {
avcodec_close(self.aacCodecCtx);
avcodec_free_context(&_aacCodecCtx);
self.aacCodecCtx = NULL;
}
if(self.aacFrame) {
av_frame_free(&_aacFrame);
self.aacFrame = NULL;
}
}
2.播放
播放PCM使用AudioQueue,具體流程:

通過上圖可以得知,Audio Queue的播放流程即是一個(gè)生產(chǎn)者與消費(fèi)者的模式:
創(chuàng)建多個(gè)Buffer容器,依次填充(生產(chǎn))Buffer后插入隊(duì)列中,開始播放(消費(fèi)),然后通過回調(diào)將消費(fèi)過的Buffer reuse,循環(huán)整個(gè)過程。
創(chuàng)建Buffer和Queue,設(shè)置參數(shù)并開始執(zhí)行隊(duì)列
- (instancetype)init
{
self = [super init];
if (self) {
str = [NSMutableString string];
sysnLock = [[NSLock alloc] init];
// 播放PCM使用
if (_audioDescription.mSampleRate <= 0) {
//設(shè)置音頻參數(shù)
_audioDescription.mSampleRate = 32000.0;//采樣率
_audioDescription.mFormatID = kAudioFormatLinearPCM;
// 下面這個(gè)是保存音頻數(shù)據(jù)的方式的說明,如可以根據(jù)大端字節(jié)序或小端字節(jié)序,浮點(diǎn)數(shù)或整數(shù)以及不同體位去保存數(shù)據(jù)
_audioDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
//1單聲道 2雙聲道
_audioDescription.mChannelsPerFrame = 1;
//每一個(gè)packet一幀數(shù)據(jù),每個(gè)數(shù)據(jù)包下的幀數(shù),即每個(gè)數(shù)據(jù)包里面有多少幀
_audioDescription.mFramesPerPacket = 1;
//每個(gè)采樣點(diǎn)16bit量化 語音每采樣點(diǎn)占用位數(shù)
_audioDescription.mBitsPerChannel = 16;
_audioDescription.mBytesPerFrame = (_audioDescription.mBitsPerChannel / 8) * _audioDescription.mChannelsPerFrame;
//每個(gè)數(shù)據(jù)包的bytes總數(shù),每幀的bytes數(shù)*每個(gè)數(shù)據(jù)包的幀數(shù)
_audioDescription.mBytesPerPacket = _audioDescription.mBytesPerFrame * _audioDescription.mFramesPerPacket;
}
// 使用player的內(nèi)部線程播放 新建輸出
AudioQueueNewOutput(&_audioDescription, AudioPlayerAQInputCallback, (__bridge void * _Nullable)(self), nil, 0, 0, &audioQueue);
// 設(shè)置音量
AudioQueueSetParameter(audioQueue, kAudioQueueParam_Volume, 1.0);
// 初始化需要的緩沖區(qū)
for (int i = 0; i < QUEUE_BUFFER_SIZE; i++) {
audioQueueBufferUsed[i] = false;
osState = AudioQueueAllocateBuffer(audioQueue, MIN_SIZE_PER_FRAME, &audioQueueBuffers[i]);
NSLog(@"AudioQueueAllocateBuffer, osState=%d", osState);
}
osState = AudioQueueStart(audioQueue, NULL);
if (osState != noErr) {
NSLog(@"AudioQueueStart Error");
}
}
return self;
}
填充Buffer
// 填充buffer
- (void)playWithData:(NSData *)data {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self->sysnLock lock];
self->tempData = [NSMutableData new];
[self->tempData appendData:data];
NSUInteger len = self->tempData.length;
Byte *bytes = (Byte *)malloc(len);
[self->tempData getBytes:bytes length:len];
int i = 0;
//判斷buffer是否被使用
while (true) {
usleep(1000);//防止cpu過高
if (!self->audioQueueBufferUsed[i]) {
self->audioQueueBufferUsed[i] = true;
break;
}else {
i++;
if (i >= QUEUE_BUFFER_SIZE) {
i = 0;
}
}
}
if (self->str.length < 3) {
[self->str appendString:[NSString stringWithFormat:@"%d",i]];
}
else if (self->str.length == 3) {
[self->str deleteCharactersInRange:NSMakeRange(0, 1)];
[self->str appendString:[NSString stringWithFormat:@"%d",i]];
}
if ([self->str isEqualToString:@"000"]) {
//reset
[self resetPlay];
}
//向buffer填充數(shù)據(jù)
self->audioQueueBuffers[i]->mAudioDataByteSize = (unsigned int)len;
memcpy(self->audioQueueBuffers[i]->mAudioData, bytes, len);
free(bytes);
//將buffer插入隊(duì)列
AudioQueueEnqueueBuffer(self->audioQueue, self->audioQueueBuffers[i], 0, NULL);
[self->sysnLock unlock];
});
}
在回調(diào)中將容器狀態(tài)設(shè)置為空,用于循環(huán)復(fù)用
// 回調(diào)
static void AudioPlayerAQInputCallback(void* inUserData,AudioQueueRef audioQueueRef, AudioQueueBufferRef audioQueueBufferRef) {
PCMPlayer *player = (__bridge PCMPlayer*)inUserData;
[player resetBufferState:audioQueueRef and:audioQueueBufferRef];
}
- (void)resetBufferState:(AudioQueueRef)audioQueueRef and:(AudioQueueBufferRef)audioQueueBufferRef {
for (int i = 0; i < QUEUE_BUFFER_SIZE; i++) {
// 將這個(gè)buffer設(shè)為未使用
if (audioQueueBufferRef == audioQueueBuffers[i]) {
audioQueueBufferUsed[i] = false;
}
}
}
- (void)dealloc {
if (audioQueue != nil) {
AudioQueueStop(audioQueue,true);
}
audioQueue = nil;
sysnLock = nil;
}
PS:Audio Queue在播放過程中可能遇到播放一會(huì)兒過后音頻開始卡頓然后音頻逐漸消失的問題,作者在代碼中的解決方法是reset:
- (void)resetPlay {
if (audioQueue != nil) {
AudioQueueReset(audioQueue);
}
}
以上,則完成了實(shí)時(shí)解碼并播放的整個(gè)流程。本文應(yīng)用場(chǎng)景基于監(jiān)控?cái)z像頭與手機(jī)客戶端的雙向?qū)崟r(shí)語音對(duì)講。
Demo地址:https://github.com/XuningZhai/TalkDemo
支持G711的Demo地址:https://github.com/XuningZhai/TalkDemo_G711_AAC