UDP实时语音通信

作者: 一叶菜 | 来源:发表于2017-01-05 13:28 被阅读1054次

    最近在做UDP实时语音通信,采用了GCDAsyncUdpSocket进行UDP传输,音频使用的是AudioUnit,录音回调和播放回调那里使用的TPCircularBuffer进行音频环形缓冲。由于录音和从服务器收到的语音数据都是PCM编码格式的,8K的采样率,每20ms一帧,数据比较大,也耗流量,因此采用了opus进行编码解码。编码之后发现数据明显变小,以前是512长度的,现在是20左右的长度,因为opus编码后是不定长的。使用APP跟手机通话时,由于APP发过去的语音数据,不仅录下了说话者的声音还把对方从扬声器播放的声音也一起录下来传给对方,所以产生了回音。而对方手机发过来的语音,是经过手机系统进行了回音消除的,所以我们听不到自己的声音。项目里采用了webRtcAEC模块进行了回音消除。

    UDP创建那里修改了一下,之前socket使用的代理队列是dispatch_get_main_queue(),现在改成了自己创建的串行队列DISPATCH_QUEUE_SERIAL

    //    self.udpSocket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
        
        self.udpSocket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:self.socketQueue];
    
    
    - (dispatch_queue_t)socketQueue {
        if (_socketQueue == nil) {
            _socketQueue = dispatch_queue_create("com.sendSocket", DISPATCH_QUEUE_SERIAL);
        }
        return _socketQueue;
    }
    

    使用串行队列作为代理队列之后,发数据那里也要使用代理队列,不可用使用其他线程来设置本socket

    //发数据到UDP
    -(int) vopc_send_dataA:(const void *)data length:(int)len
    {
        DLog(@"=====发数据到UDP时加协议头");
        dispatch_async(self.socketQueue, ^{
    
            NSData *data1=[NSData dataWithBytes:data  length:320];
            
            unsigned char outBuf[320];
            short *readBuf = NULL;
            readBuf = (short *)[data1 bytes];
            
           //回音消除Process
            int aceProcess = WebRtcAecm_Process(self.waveIO.AecmInst, (short *)refBuf, readBuf, (short *)outBuf, 160, 60);
            
            DLog(@"===aceProcess:%d",aceProcess);
            
            NSData *outData = [NSData dataWithBytes:(Byte *)outBuf length:320];
            //刘文静添加  opus 编码数据
            outData = [self.waveIO.opusCode encodePCMData:outData];
            
            //        DLog(@"===outData:%@",outData);
            
            outData = [self getSIMUDPData:outData];
            //        DLog(@"===加头发数据DATA:%@",outData);
            
            [self.udpSocket sendData:outData toHost:self.host port:self.hostPort withTimeout:0.0 tag:_udpTag];
            
            ++_udpTag;
            
        });
        
        return true;
    }
    

    这样可以把原先在主线程跑的数据处理拿到串行队列里来。

    在收数据那里使用了串行队列receiveQueue.

    -(dispatch_queue_t )receiveQueue
    {
        if (_receiveQueue == nil)
        {
            _receiveQueue = dispatch_queue_create("com.udp.receiveQueue", DISPATCH_QUEUE_SERIAL);
        }
        return _receiveQueue;
    }
    
    - (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data
          fromAddress:(NSData *)address
    withFilterContext:(id)filterContext
    {
        // 防止 didReceiveData 被阻塞,用个其他队列里的线程去回调 block
        dispatch_async(self.receiveQueue, ^{
            @autoreleasepool {
            
                NSData* myBlob=[[NSData alloc]init];
                DLog(@"-===UDP收到数据====didReceiveData");
                
                if (data.length>4)
                {
                    myBlob=[data subdataWithRange:NSMakeRange(4, data.length-4)];
    
                }
                
                //刘文静添加 收到数据 opus解码
                myBlob = [self.waveIO.opusCode decodeOpusData:myBlob];
                //    DLog(@"===UDP收到语音解压数据data:%@",myBlob);
                
                NSInteger len=myBlob.length;
                if (len>0)
                {
                    if (len==320)
                    {
                        if(len%320==0)
                        {
                           // DLog(@"===解码后数据长度len/320==0");
                            
                            NSUInteger length = [myBlob length];
                            NSUInteger offset = 0;
                            do {
                                NSUInteger thisChunkSize=320;
                                NSData* chunk = [NSData dataWithBytesNoCopy:(char *)[myBlob bytes] + offset
                                                                     length:thisChunkSize
                                                               freeWhenDone:NO];
                                //                    DLog(@"--==解码后=-chunk:%@",chunk);
                                
                                memcpy(refBuf,[chunk bytes], 320);  
                                int aecBuffer =  WebRtcAecm_BufferFarend(self.waveIO.AecmInst, (short *)refBuf, 160);
                                DLog(@"===aecBuffer:%d",aecBuffer);
                                
                                [self procAUdpPack:chunk];
                                
                                offset += thisChunkSize;
                                
                            } while (offset < length);
                        }else
                        {
                            DLog(@"--====I can not handle half package,%ld",(long)len);
                        }
                        return;
                    }
                    else
                    {
                        [self procAUdpPack:myBlob];
                    }
                }
            }
        });
    }
    

    在使用opus编码解码后,CPU的使用率上去了。看到有很多人使用ProtocolBuffer。那么为什么要用ProtocolBuffer呢?它是一种二进制格式,免去了文本格式转换的各种困扰,并且转换效率非常快,由于它的跨平台、跨编程语言的特点,让它越来越普及,尤其是网络数据交换方面日趋成为一种主流.这个序列化成的二进制的包比其他传输格式小很多,可以节约网络流量。后面优化时,或许可以尝试一下。

    相关文章

    ProtocolBuffer for Objective-C 运行环境配置及使用
    iOS 下Opus 压缩PCM音频数据方法
    iOS下Opus 编译教程
    视频通话之音频(介绍的很详细)
    iOS音频播放 (二):AudioSession

    相关文章

      网友评论

      • 7aca3d60d86e:嗯,我最近也在研究udp实时返音这块,但是目前卡在了回音消除的算法上,楼主的思路很清晰,但是针对传入refBuf和readbuf还有些疑惑,比如发送端的refbuf获取,楼主可方便分享下源码呢?

      本文标题:UDP实时语音通信

      本文链接:https://www.haomeiwen.com/subject/liqzvttx.html