iOS語音對(duì)講(三)FFmpeg實(shí)時(shí)解碼AAC并播放PCM

本文介紹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 Process

通過上圖可以得知,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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容