zoukankan      html  css  js  c++  java
  • iOS 实时录音和播放

    需求:最近公司需要做一个楼宇对讲的功能:门口机(连接WIFI)拨号对室内机(对应的WIFI)的设备进行呼叫,室内机收到呼叫之后将对收到的数据进行UDP广播的转发,手机(连接对应的WIFI)收到视频流之后,实时的展示视频数据(手机可以接听,挂断,手机接听之后,室内机不展示视频,只是进行转发。)

    简单点说就是手机客户端需要做一个类似于直播平台的软件,可以实时的展示视频,实时的播放接收到的声音数据,并且实时将手机麦克风收到的声音回传给室内机,室内机负责转发给门口机。

    这篇文章介绍iOS怎么进行实时的录音和播放收到的声音数据 

    想要使用系统的框架实时播放声音和录音数据,就得知道音频队列服务,

    在AudioToolbox框架中的音频队列服务,它完全可以做到音频播放和录制

    一个音频服务队列有三个部分组成:

    1.三个缓冲器Buffers:每个缓冲器都是一个存储音频数据的临时仓库。
    2.一个缓冲队列Buffer Queue:一个包含音频缓冲器的有序队列。
    3.一个回调CallBack:一个自定义的队列回调函数。

     具体怎么运转的还是百度吧!

    我的简单理解:

    对于播放:系统会自动从缓冲队列中循环取出每个缓冲器中的数据进行播放,我们需要做的就是将接收到的数据循环的放到缓冲器中,剩下的就交给系统去实现了。

    对于录音:  系统会自动将录的声音放入队列中的每个缓冲器中,我们需要做的就是从回调函数中将数据转化我们自己的数据就OK了。

    #pragma mark--实时播放

    1. 导入系统框架AudioToolbox.framework  AVFoundation.framework

    2. 获取麦克风权限,在工程的Info.plist文件中加入Privacy - Microphone Usage Description 这个key 描述:App想要访问您的麦克风

    3. 创建播放声音的类 EYAudio

    EYAudio.h

    #import <Foundation/Foundation.h>
    
    @interface EYAudio : NSObject
    // 播放的数据流数据 - (void)playWithData:(NSData *)data; // 声音播放出现问题的时候可以重置一下 - (void)resetPlay; // 停止播放 - (void)stop; @end

     EYAudio.m

    #import "EYAudio.h"
    #import <AVFoundation/AVFoundation.h>
    #import <AudioToolbox/AudioToolbox.h>
    
    #define MIN_SIZE_PER_FRAME 1920   //每个包的大小,室内机要求为960,具体看下面的配置信息
    #define QUEUE_BUFFER_SIZE  3      //缓冲器个数
    #define SAMPLE_RATE        16000  //采样频率
    
    @interface EYAudio(){
        AudioQueueRef audioQueue;                                 //音频播放队列
        AudioStreamBasicDescription _audioDescription;
        AudioQueueBufferRef audioQueueBuffers[QUEUE_BUFFER_SIZE]; //音频缓存
        BOOL audioQueueBufferUsed[QUEUE_BUFFER_SIZE];             //判断音频缓存是否在使用
        NSLock *sysnLock;
        NSMutableData *tempData;
        OSStatus osState;
    }
    @end
    
    @implementation EYAudio
    
    #pragma mark - 提前设置AVAudioSessionCategoryMultiRoute 播放和录音
    + (void)initialize
    {
        NSError *error = nil;
        //只想要播放:AVAudioSessionCategoryPlayback
        //只想要录音:AVAudioSessionCategoryRecord
        //想要"播放和录音"同时进行 必须设置为:AVAudioSessionCategoryMultiRoute 而不是AVAudioSessionCategoryPlayAndRecord(设置这个不好使)
        BOOL ret = [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryMultiRoute error:&error];
        if (!ret) {
            NSLog(@"设置声音环境失败");
            return;
        }
        //启用audio session
        ret = [[AVAudioSession sharedInstance] setActive:YES error:&error];
        if (!ret)
        {
            NSLog(@"启动失败");
            return;
        }
    }
    
    - (void)resetPlay
    {
        if (audioQueue != nil) {
            AudioQueueReset(audioQueue);
        }
    }
    
    - (void)stop
    {
        if (audioQueue != nil) {
            AudioQueueStop(audioQueue,true);
        }
    
        audioQueue = nil;
        sysnLock = nil;
    }
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            sysnLock = [[NSLock alloc]init];
    
            //设置音频参数 具体的信息需要问后台
            _audioDescription.mSampleRate = SAMPLE_RATE;
            _audioDescription.mFormatID = kAudioFormatLinearPCM;
            _audioDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
            //1单声道
            _audioDescription.mChannelsPerFrame = 1;
            //每一个packet一侦数据,每个数据包下的桢数,即每个数据包里面有多少桢
            _audioDescription.mFramesPerPacket = 1;
            //每个采样点16bit量化 语音每采样点占用位数
            _audioDescription.mBitsPerChannel = 16;
            _audioDescription.mBytesPerFrame = (_audioDescription.mBitsPerChannel / 8) * _audioDescription.mChannelsPerFrame;
            //每个数据包的bytes总数,每桢的bytes数*每个数据包的桢数
            _audioDescription.mBytesPerPacket = _audioDescription.mBytesPerFrame * _audioDescription.mFramesPerPacket;
    
            // 使用player的内部线程播放 新建输出
            AudioQueueNewOutput(&_audioDescription, AudioPlayerAQInputCallback, (__bridge void * _Nullable)(self), nil, 0, 0, &audioQueue);
    
            // 设置音量
            AudioQueueSetParameter(audioQueue, kAudioQueueParam_Volume, 1.0);
    
            // 初始化需要的缓冲区
            for (int i = 0; i < QUEUE_BUFFER_SIZE; i++) {
                audioQueueBufferUsed[i] = false;
                osState = AudioQueueAllocateBuffer(audioQueue, MIN_SIZE_PER_FRAME, &audioQueueBuffers[i]);
            }
    
            osState = AudioQueueStart(audioQueue, NULL);
            if (osState != noErr) {
                NSLog(@"AudioQueueStart Error");
            }
        }
        return self;
    }
    
    // 播放数据
    -(void)playWithData:(NSData *)data
    {
        [sysnLock lock];
    
        tempData = [NSMutableData new];
        [tempData appendData: data];
        NSUInteger len = tempData.length;
        Byte *bytes = (Byte*)malloc(len);
        [tempData getBytes:bytes length: len];
    
        int i = 0;
        while (true) {
            if (!audioQueueBufferUsed[i]) {
                audioQueueBufferUsed[i] = true;
                break;
            }else {
                i++;
                if (i >= QUEUE_BUFFER_SIZE) {
                    i = 0;
                }
            }
        }
    
        audioQueueBuffers[i] -> mAudioDataByteSize =  (unsigned int)len;
        // 把bytes的头地址开始的len字节给mAudioData,向第i个缓冲器
        memcpy(audioQueueBuffers[i] -> mAudioData, bytes, len);
    
        // 释放对象
        free(bytes);
    
        //将第i个缓冲器放到队列中,剩下的都交给系统了
        AudioQueueEnqueueBuffer(audioQueue, audioQueueBuffers[i], 0, NULL);
    
        [sysnLock unlock];
    }
    
    // ************************** 回调 **********************************
    // 回调回来把buffer状态设为未使用
    static void AudioPlayerAQInputCallback(void* inUserData,AudioQueueRef audioQueueRef, AudioQueueBufferRef audioQueueBufferRef) {
    
        EYAudio* audio = (__bridge EYAudio*)inUserData;
    
        [audio resetBufferState:audioQueueRef and:audioQueueBufferRef];
    }
    
    - (void)resetBufferState:(AudioQueueRef)audioQueueRef and:(AudioQueueBufferRef)audioQueueBufferRef {
        // 防止空数据让audioqueue后续都不播放,为了安全防护一下
        if (tempData.length == 0) {
            audioQueueBufferRef->mAudioDataByteSize = 1;
            Byte* byte = audioQueueBufferRef->mAudioData;
            byte = 0;
            AudioQueueEnqueueBuffer(audioQueueRef, audioQueueBufferRef, 0, NULL);
        }
    
        for (int i = 0; i < QUEUE_BUFFER_SIZE; i++) {
            // 将这个buffer设为未使用
            if (audioQueueBufferRef == audioQueueBuffers[i]) {
                audioQueueBufferUsed[i] = false;
            }
        }
    }
    
    @end

    外界使用: 不断调用下面的方法将NSData传递进来

    - (void)playWithData:(NSData *)data; 

     #pragma mark--实时录音

    1. 导入系统框架AudioToolbox.framework  AVFoundation.framework

    2. 创建录音的类 EYRecord

    EYRecord.h

    #import <Foundation/Foundation.h>
    
    @interface ESARecord : NSObject
    
    //开始录音
    - (void)startRecording;
    
    //停止录音
    - (void)stopRecording;
    
    @end

    EYRecord.m

    #import "ESARecord.h"
    #import <AudioToolbox/AudioToolbox.h>
    
    #define QUEUE_BUFFER_SIZE 3      // 输出音频队列缓冲个数
    #define kDefaultBufferDurationSeconds 0.03//调整这个值使得录音的缓冲区大小为960,实际会小于或等于960,需要处理小于960的情况
    #define kDefaultSampleRate 16000   //定义采样率为16000
    
    extern NSString * const ESAIntercomNotifationRecordString;
    
    static BOOL isRecording = NO;
    
    @interface ESARecord(){
        AudioQueueRef _audioQueue;                          //输出音频播放队列
        AudioStreamBasicDescription _recordFormat;
        AudioQueueBufferRef _audioBuffers[QUEUE_BUFFER_SIZE]; //输出音频缓存
    }
    @property (nonatomic, assign) BOOL isRecording;
    
    @end
    
    @implementation ESARecord
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            //重置下
            memset(&_recordFormat, 0, sizeof(_recordFormat));
            _recordFormat.mSampleRate = kDefaultSampleRate;
            _recordFormat.mChannelsPerFrame = 1;
            _recordFormat.mFormatID = kAudioFormatLinearPCM;
    
            _recordFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
            _recordFormat.mBitsPerChannel = 16;
            _recordFormat.mBytesPerPacket = _recordFormat.mBytesPerFrame = (_recordFormat.mBitsPerChannel / 8) * _recordFormat.mChannelsPerFrame;
            _recordFormat.mFramesPerPacket = 1;
    
            //初始化音频输入队列
            AudioQueueNewInput(&_recordFormat, inputBufferHandler, (__bridge void *)(self), NULL, NULL, 0, &_audioQueue);
    
            //计算估算的缓存区大小
            int frames = (int)ceil(kDefaultBufferDurationSeconds * _recordFormat.mSampleRate);
            int bufferByteSize = frames * _recordFormat.mBytesPerFrame;
    
            NSLog(@"缓存区大小%d",bufferByteSize);
    
            //创建缓冲器
            for (int i = 0; i < QUEUE_BUFFER_SIZE; i++){
                AudioQueueAllocateBuffer(_audioQueue, bufferByteSize, &_audioBuffers[i]);
                AudioQueueEnqueueBuffer(_audioQueue, _audioBuffers[i], 0, NULL);
            }
        }
        return self;
    }
    
    -(void)startRecording
    {
        // 开始录音
        AudioQueueStart(_audioQueue, NULL);
        isRecording = YES;
    }
    
    void inputBufferHandler(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *inStartTime,UInt32 inNumPackets, const AudioStreamPacketDescription *inPacketDesc)
    {
        if (inNumPackets > 0) {
            ESARecord *recorder = (__bridge ESARecord*)inUserData;
            [recorder processAudioBuffer:inBuffer withQueue:inAQ];
        }
        
        if (isRecording) {
            AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
        }
    }
    
    - (void)processAudioBuffer:(AudioQueueBufferRef )audioQueueBufferRef withQueue:(AudioQueueRef )audioQueueRef
    {
        NSMutableData * dataM = [NSMutableData dataWithBytes:audioQueueBufferRef->mAudioData length:audioQueueBufferRef->mAudioDataByteSize];
        
        if (dataM.length < 960) { //处理长度小于960的情况,此处是补00
            Byte byte[] = {0x00};
            NSData * zeroData = [[NSData alloc] initWithBytes:byte length:1];
            for (NSUInteger i = dataM.length; i < 960; i++) {
                [dataM appendData:zeroData];
            }
        }
    
        // NSLog(@"实时录音的数据--%@", dataM);
        //此处是发通知将dataM 传递出去
        [[NSNotificationCenter defaultCenter] postNotificationName:@"EYRecordNotifacation" object:@{@"data" : dataM}];
    }
    
    -(void)stopRecording
    {
        if (isRecording)
        {
            isRecording = NO;
            
            //停止录音队列和移除缓冲区,以及关闭session,这里无需考虑成功与否
            AudioQueueStop(_audioQueue, true);
            //移除缓冲区,true代表立即结束录制,false代表将缓冲区处理完再结束
            AudioQueueDispose(_audioQueue, true);
        }
        NSLog(@"停止录音");
    }
    
    @end

    如果不好使尝试将 EYRecord.m ----> EYRecord.mm

     更多内容--> 博客导航 每周一篇哟!!!

    有任何关于iOS开发的问题!欢迎下方留言!!!或者邮件lieryangios@126.com 虽然我不一定能够解答出来,但是我会请教iOS开发高手!!!解答您的问题!!!

  • 相关阅读:
    npm总是安装不成功,而且很慢?
    Nginx启动报错:10013: An attempt was made to access a socket in a way forbidden
    firebug如何使用
    video详解 HTML5中的视频:
    树的各种遍历
    SQL语句执行顺序
    vim常用命令
    无监督分类算法—K-Means
    Json字符串和Json对象的简单总结
    List拆分成多个集合
  • 原文地址:https://www.cnblogs.com/CoderEYLee/p/Object-C-0026.html
Copyright © 2011-2022 走看看