• iOS语音通话(语音对讲)


    中间参考了别人的Demo,下载地址不记得了。

    因为项目需要做一个语音对讲功能,其实说白了就是类似QQ的语音通话,但是资料少之又少,研究了好久,才跟同事弄出一个粗略的版本。我记性不好,所以来记录一下,也希望能够帮助其他人。

    本来以为是要做语音对讲,类似微信的发送语音,我觉得这个还挺简单的,就是发送一个语音的文件,所以一开始用的是AVAudioPlayer,因为这个东西只能播放本地音频,而且非常简单。可是都快做好了,头头才说明白要的是语音通话。(小公司,别说文档了,连接口文档都没有)

    后来找到AudioQueue,找了好多demo和资料,都没有直接播放从服务器端接收到的数据的例子,后来没办法,只能自己想办法咯。不过大致过程是一致的。

    首先肯定是设置创建录音的音频队列,以及缓冲区,还有播放的队列和播放缓冲区,因为我们是要一起打开,所以一起创建,开始录音,并播放声音。

    后面会上传demo,开始对讲的方法如下:

    //开始对讲
    - (IBAction)startIntercom:(id)sender {
        //让udpSocket 开始接收数据
        [self.udpSocket beginReceiving:nil];
        //先把接收数组清空
        if (receiveData) {
            receiveData = nil;
        }
        receiveData = [[NSMutableArray alloc] init];
        
        if (_recordAmrCode == nil) {
            _recordAmrCode = [[RecordAmrCode alloc] init];
        }
        //设置录音的参数
        [self setupAudioFormat:kAudioFormatLinearPCM SampleRate:kDefaultSampleRate];
        _audioFormat.mSampleRate = kDefaultSampleRate;
        //创建一个录制音频队列
        AudioQueueNewInput (&(_audioFormat),GenericInputCallback,(__bridge void *)self,NULL,NULL,0,&_inputQueue);
        //创建一个输出队列
        AudioQueueNewOutput(&_audioFormat, GenericOutputCallback, (__bridge void *) self, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 0,&_outputQueue);
        //设置话筒属性等
        [self initSession];
        
        NSError *error = nil;
        //设置audioSession格式 录音播放模式
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
        
        UInt32 audioRouteOverride = kAudioSessionOverrideAudioRoute_Speaker;  //设置成话筒模式
        AudioSessionSetProperty (kAudioSessionProperty_OverrideAudioRoute,
                                 sizeof (audioRouteOverride),
                                 &audioRouteOverride);
        
        //创建录制音频队列缓冲区
        for (int i = 0; i < kNumberAudioQueueBuffers; i++) {
            AudioQueueAllocateBuffer (_inputQueue,kDefaultInputBufferSize,&_inputBuffers[i]);
            
            AudioQueueEnqueueBuffer (_inputQueue,(_inputBuffers[i]),0,NULL);
        }
        
        //创建并分配缓冲区空间 4个缓冲区
        for (int i = 0; i<kNumberAudioQueueBuffers; ++i)
        {
            AudioQueueAllocateBuffer(_outputQueue, kDefaultOutputBufferSize, &_outputBuffers[i]);
        }
        for (int i=0; i < kNumberAudioQueueBuffers; ++i) {
            makeSilent(_outputBuffers[i]);  //改变数据
            // 给输出队列完成配置
            AudioQueueEnqueueBuffer(_outputQueue,_outputBuffers[i],0,NULL);
        }
        
        Float32 gain = 1.0;                                       // 1
        // Optionally, allow user to override gain setting here 设置音量
        AudioQueueSetParameter (_outputQueue,kAudioQueueParam_Volume,gain);
        
        //开启录制队列
        AudioQueueStart(self.inputQueue, NULL);
        //开启播放队列
        AudioQueueStart(_outputQueue,NULL);
        
        [_startButton setEnabled:NO];
        [_stopButton setEnabled:YES];
        
    }
    

    然后就是实现录音和播放的回调,录音回调中对PCM数据编码,打包。代码如下:

    //录音回调
    void GenericInputCallback (
                               void                                *inUserData,
                               AudioQueueRef                       inAQ,
                               AudioQueueBufferRef                 inBuffer,
                               const AudioTimeStamp                *inStartTime,
                               UInt32                              inNumberPackets,
                               const AudioStreamPacketDescription  *inPacketDescs
                               )
    {
        NSLog(@"录音回调方法");
        RootViewController *rootCtrl = (__bridge RootViewController *)(inUserData);
        
        if (inNumberPackets > 0) {
            NSData *pcmData = [[NSData alloc] initWithBytes:inBuffer->mAudioData length:inBuffer->mAudioDataByteSize];
            //pcm数据不为空时,编码为amr格式
            if (pcmData && pcmData.length > 0) {
                NSData *amrData = [rootCtrl.recordAmrCode encodePCMDataToAMRData:pcmData];
                //这里是对编码后的数据,通过socket发送到另一个客户端
                [rootCtrl.udpSocket sendData:amrData toHost:kDefaultIP port:kDefaultPort withTimeout:-1 tag:0];
                
                
            }
            
        }
        AudioQueueEnqueueBuffer (inAQ,inBuffer,0,NULL);
        
    }
    

    // 输出回调
    void GenericOutputCallback (
                                void                 *inUserData,
                                AudioQueueRef        inAQ,
                                AudioQueueBufferRef  inBuffer
                                )
    {
        NSLog(@"播放回调");
        RootViewController *rootCtrl = (__bridge RootViewController *)(inUserData);
        NSData *pcmData = nil;
        if([receiveData count] >0)
        {
            NSData *amrData = [receiveData objectAtIndex:0];
            
            pcmData = [rootCtrl.recordAmrCode decodeAMRDataToPCMData:amrData];
            
            if (pcmData) {
                if(pcmData.length < 10000){
                    memcpy(inBuffer->mAudioData, pcmData.bytes, pcmData.length);
                    inBuffer->mAudioDataByteSize = (UInt32)pcmData.length;
                    inBuffer->mPacketDescriptionCount = 0;
                }
            }
            [receiveData removeObjectAtIndex:0];
        }
        else
        {
            makeSilent(inBuffer);
        }
        AudioQueueEnqueueBuffer(rootCtrl.outputQueue,inBuffer,0,NULL);
    }
    


    然后就是socket接收数据了,是个代理方法:

    #pragma mark - GCDAsyncUdpSocketDelegate
    - (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data
          fromAddress:(NSData *)address
    withFilterContext:(id)filterContext
    {
        //这里因为对录制的PCM数据编码为amr格式并添加RTP包头之后的大小,大家可以根据自己的协议,在包头中封装上数据长度再来解析。
        //PS:因为socket在发送过程中会粘包,如发送数据AAA,然后再发送BBB,可能会一次收到AAABBB,也可能会一次收到AAA,另一次收到BBB,所以针对这种情况要判断接收数据大小,来拆包
        if(data.length >667)
        {
            int num = (data.length)/667;
            int sum = 0;
            for (int i=0; i<num; i++)
            {
                NSData *receviceData = [data subdataWithRange:NSMakeRange(i*667,667)];
                [receiveData addObject:receviceData];
                sum = sum+667;
            }
            if(sum < data.length)
            {
                NSData *otherData = [data subdataWithRange:NSMakeRange(sum, (data.length-sum))];
                [receiveData addObject:otherData];
            }
        }
        else
        {
            [receiveData addObject:data];
        }
        
    }
    
    待会上传demo。但是demo是点对点发送的。

    PS:附送一点思路,服务器端做一个对讲的服务器,然后所有人都用SOCKET 的TCP方式连接对讲服务器的IP和端口号,然后我们把编码后的数据发送给服务器,通过服务器转发给其他人。

    上传的代码里除了有amr编码,还加了RTP包头,我等会上传一个不含RTP包头的,只是把PCM数据编码为AMR格式,把AMR格式数据解码为PCM数据的类文件。至于怎么把文件转码为AMR格式,网上的demo太多咯。

    Demo和一个不含RTP包头的编码类

  • 相关阅读:
    SQLSERVER 的表分区(水平) 操作记录2
    GraphQl in ASP.NET Core
    初始认知学习 .net core 逐步加深
    C# 关于使用JavaScriptSerializer 序列化与返序列化的操作
    Nginx、IIS 相关命令
    SqlServer:查询指定表所有外键关联表信息
    centos 重启宝塔命令
    c# 根据日志中的方法信息,反射再次执行相关方法
    jackson 下载地址记录
    【设计模式】六大原则
  • 原文地址:https://www.cnblogs.com/wanghang/p/6298890.html
Copyright © 2020-2023  润新知