• ios 实时音频流获取---分解LFLiveKit


    AVRecorder: 录制成音频文件,无法直接获取实时音频数据;

    AudioQueue:可以生成音频文件,可直接实时获取音频数据,数据回调有延迟,根据缓冲区大小延迟在20ms~1s

    AudioUnit:可以生成音频文件,可直接实时获取音频数据,数据回调较低延迟,基本维持在20ms左右

    以上数据延迟参考 https://www.cnblogs.com/decwang/p/4701125.html

    概念解读:

    参考:https://www.jianshu.com/p/f859640fcb33 & https://www.cnblogs.com/try2do-neo/p/3278459.html

    对于通用的audioUnit,可以有1-2条输入输出流,输入和输出不一定相等。

    每个element表示一个音频处理上下文(context), 也称为bus。

    每个element有输出和输出部分,称为scope,分别是input scope和Output scope。

    Global scope确定只有一个element,就是element0,有些属性只能在Global scope上设置。

    对于remote_IO类型audioUnit,即从硬件采集和输出到硬件的audioUnit,

    它的逻辑是固定的:固定2个element,麦克风经过element1到APP,APP经element0到扬声器。

       
     
     
    AudioUnit录音逻辑如下:
    根据 设置的音频组件特性 

    AudioComponentDescription 

    寻找一个最适合的音频组件

    AudioComponentFindNext

    然后创建一个音频组件对象

    AudioComponentInstanceNew

    ,设置这个音频组件对象的属性的值

    AudioUnitSetProperty

    ,设置数据回调

    AURenderCallbackStruct

    ,初始化音频这个组件对象

    AudioUnitInitialize

    启动录音,持续收到音频数据回调。
     
    代码分解
     
    @property (nonatomic, assign) AudioComponentInstance componetInstance; /* 代表一个特定的音频组件对象 */
    @property (nonatomic, assign) AudioComponent component; /* 代表一个特定的音频组件类 */
    @property (nonatomic, strong) dispatch_queue_t taskQueue;
    @property (nonatomic, assign) BOOL isRunning;
    @property (nonatomic, strong,nullable) LFLiveAudioConfiguration *configuration;
    - (instancetype)initWithAudioConfiguration:(LFLiveAudioConfiguration *)configuration{
        if(self = [super init]){
            _configuration = configuration;
            self.isRunning = NO;
            self.taskQueue = dispatch_queue_create("com.youku.Laifeng.audioCapture.Queue", NULL);
            
            AVAudioSession *session = [AVAudioSession sharedInstance];
            
            /* 音频线路切换监听(例如:突然插入耳机 或 链接蓝牙等) */
            [[NSNotificationCenter defaultCenter] addObserver: self
                                                     selector: @selector(handleRouteChange:)
                                                         name: AVAudioSessionRouteChangeNotification
                                                       object: session];
            /* 录音功能被打断监听(例:来电铃声) */
            [[NSNotificationCenter defaultCenter] addObserver: self
                                                     selector: @selector(handleInterruption:)
                                                         name: AVAudioSessionInterruptionNotification
                                                       object: session];
            
            /* 用于描述一个音频组件的独特性和识别ID的结构体 */
            AudioComponentDescription acd;
            
            /* 音频组件主类型: 输出类型 */
            acd.componentType = kAudioUnitType_Output;
            //acd.componentSubType = kAudioUnitSubType_VoiceProcessingIO;
            /* 音频组件的子类型: RemoteIO,即从硬件采集和输出到硬件的audioUnit,它的逻辑是固定的:固定2个element,麦克风经过element1到APP,APP经element0到扬声器。 */
            acd.componentSubType = kAudioUnitSubType_RemoteIO;
            /* 供应商标识 */
            acd.componentManufacturer = kAudioUnitManufacturer_Apple;
            /* must be set to zero unless a known specific value is requested */
            acd.componentFlags = 0;
            acd.componentFlagsMask = 0;
          
            /* 找到一个最适合以上描述信息的音频组件类 */
            self.component = AudioComponentFindNext(NULL, &acd);
            
            OSStatus status = noErr;
         
            /* 创建一个音频组件实例(对象),根据给定的音频组件类。*/
            status = AudioComponentInstanceNew(self.component, &_componetInstance);
            
            if (noErr != status) {
                [self handleAudioComponentCreationFailure];
            }
            
            UInt32 flagOne = 1;
            
            /*
              设置 打开音频组件对象 从系统硬件麦克风到APP 的IO通道
             
             param1: 音频组件对象
             param2: 打开IO通道
                     默认情况element0,也就是从APP到扬声器的IO时打开的,而element1,即从麦克风到APP的IO是关闭的。
             param3: 设置为输入(音频数据输入到App)
             param4: 设置为element1(从麦克风到APP的IO)
             param5: 设置为启动(1 代表启动/打开)
             param6: flagOne的字节数
             */
            AudioUnitSetProperty(self.componetInstance,
                                 kAudioOutputUnitProperty_EnableIO,
                                 kAudioUnitScope_Input,
                                 1,
                                 &flagOne,
                                 sizeof(flagOne));
            
            /* 这个结构体封装了音频流的所有属性信息 */
            AudioStreamBasicDescription desc = {0};
            /* 采样率(每秒采集的样本数 单位hz) */
            desc.mSampleRate = _configuration.audioSampleRate;
            /* 音频格式 PCM */
            desc.mFormatID = kAudioFormatLinearPCM;
            /**/
            desc.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
            /* 每一帧数据的通道数 */
            desc.mChannelsPerFrame = (UInt32)_configuration.numberOfChannels;
            /* 每一个数据包中有多少帧 */
            desc.mFramesPerPacket = 1;
            /* 每个通道的采样位数(采样精度,默认16bits) */
            desc.mBitsPerChannel = 16;
            /* 每一帧数据有多少字节(1byts=8bits)*/
            desc.mBytesPerFrame = desc.mBitsPerChannel / 8 * desc.mChannelsPerFrame;
            /* 每个数据包中有多少字节 */
            desc.mBytesPerPacket = desc.mBytesPerFrame * desc.mFramesPerPacket;
    
            
            /* 用于处理音频数据回调的结构体 */
            AURenderCallbackStruct cb;
            /* 回调函数执行时传递给它的参数,这里把self作为参数传递就可以拿到当前类公开的数据信息 */
            cb.inputProcRefCon = (__bridge void *)(self);
            cb.inputProc = handleInputBuffer; // 回调函数
            
            /*
             设置 从系统硬件麦克风到APP的 音频流的 输入格式
            
             param1: 音频组件对象
             param2: 音频单元设置为流的格式
             param3: 设置为输出(从麦克风输入到app)
             param4: 设置为element1(从麦克风到APP)
             param5: 音频流的描述
             param6: 字节数
             */
            AudioUnitSetProperty(self.componetInstance,
                                 kAudioUnitProperty_StreamFormat,
                                 kAudioUnitScope_Output,
                                 1,
                                 &desc,
                                 sizeof(desc));
            
            /* 设置 APP收到输入数据 的回调函数 (app收到音频数据就会触发回调函数)
             kAudioUnitScope_Global: 只有一个element,就是element0,有些属性只能在Global scope上设置。
             */
            AudioUnitSetProperty(self.componetInstance,
                                 kAudioOutputUnitProperty_SetInputCallback,
                                 kAudioUnitScope_Global,
                                 1,
                                 &cb,
                                 sizeof(cb));
            
            /*
             初始化音频单元
             */
            status = AudioUnitInitialize(self.componetInstance);
            
            if (noErr != status) {
                [self handleAudioComponentCreationFailure];
            }
            
            [session setPreferredSampleRate:_configuration.audioSampleRate error:nil];
            [session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers error:nil];
            [session setActive:YES withOptions:kAudioSessionSetActiveFlag_NotifyOthersOnDeactivation error:nil];
            
            [session setActive:YES error:nil];
        }
        return self;
    }
    - (void)dealloc {
        [[NSNotificationCenter defaultCenter] removeObserver:self];
    
        dispatch_sync(self.taskQueue, ^{
            if (self.componetInstance) {
                self.isRunning = NO;
                /* 停止  从系统硬件麦克风到APP的 音频单元输出 */
                AudioOutputUnitStop(self.componetInstance);
                /* 结束当前的这个音频组件实例 */
                AudioComponentInstanceDispose(self.componetInstance);
                self.componetInstance = nil;
                self.component = nil;
            }
        });
    }
    #pragma mark -- CallBack
    static OSStatus handleInputBuffer(void *inRefCon,
                                      AudioUnitRenderActionFlags *ioActionFlags,
                                      const AudioTimeStamp *inTimeStamp,
                                      UInt32 inBusNumber,
                                      UInt32 inNumberFrames,
                                      AudioBufferList *ioData) {
        
        /*
         以《自动释放池块》降低内存峰值(应用程序在某个特定时间段内的最大内存用量)。
         释放对象有两种方式:
         A-调用用对应的release方法,使其引用计数立即递减;
         B-调用对象autoRelease方法,将其加入自动释放池中,在稍后的某个时间进行释放,当进行清空自动释放池使,系统会向池中对象发送release消息,继而池中对象执行release方法。
         自动释放池于左花括号“{”创建,右花括号“}”自动清空,池中所有对象会在末尾收到release消息。
         
         是否需要建立额外的自动释放池,要看具体情况,这里音频数据持续回调用临时变量处理,占用内存无法及时释放回收,于是用到的自动释放池。
         尽管建立@autoreleasepool其开销不大,但是毕竟还是有的。可以通过Xcode调试查看某个时间段内的内存峰值来合理安排。
         */
        @autoreleasepool {
            LFAudioCapture *source = (__bridge LFAudioCapture *)inRefCon;
            if (!source) return -1;
    
            AudioBuffer buffer;          /* 一个持有音频缓冲数据的结构体 */
            buffer.mData = NULL;         /* 一个指向音频缓冲数据的《指针》 */
            buffer.mDataByteSize = 0;    /* 缓冲数据的字节数 */
            buffer.mNumberChannels = 1;  /* 缓冲数据中的通道数(设置为单通道,降低数据量) */
    
            AudioBufferList buffers;        /* 一个填充缓冲数据对象的 动态数组 结构体 */
            buffers.mNumberBuffers = 1;     /* 数组中仅有1个缓冲数据对象*/
            buffers.mBuffers[0] = buffer;   /* 数组中有效的缓冲数据对象 */
    
            /*
             音频单元渲染
             param1: 渲染对象
             param2: 配置渲染操作的对象
             param3: 渲染操作的时间戳
             param4: 渲染的数据缓冲
             param5: 渲染的音频帧数
             param6: 渲染的音频数据放入缓冲列表中
             */
            OSStatus status = AudioUnitRender(source.componetInstance,
                                              ioActionFlags,
                                              inTimeStamp,
                                              inBusNumber,
                                              inNumberFrames,
                                              &buffers);
    
            if (source.muted) {
                /* 如果开启静音就需要将音频的缓冲地址的内存数据清空, 这样本地就不会再推音频流到服务端,达到静音母的。*/
                for (int i = 0; i < buffers.mNumberBuffers; i++) {
                    AudioBuffer ab = buffers.mBuffers[i];
                   
                    /*
                     memset(void *s,int ch,size_t n);
                     将s所指向的某一块内存中的后n个 字节的内容全部设置为ch指定的ASCII值,
                     通常用于:清空一个结构类型的变量或数组。
                     */
                    memset(ab.mData, 0, ab.mDataByteSize);
                }
            }
            
            if (!status) {
                /*
                 执行回调的两个必须条件:
                 1.委托目标对象delegate必须存在
                 2.委托目标对象delegate必须响应@selector()--->即delegate实现了selector。
                 
                 当前函数是实时持续获取音频数据,并且是频繁的被调用。
                 那么,如果第一次判断以上两个条件都成立的话,后续频繁判断就显得多余了。
                 而且委托对象本身不会变动,并不会突然不响应之前的@selector(),
                 所以,可以把委托对象对某一个协议方法的响应缓存起来,进而优化运行效率。
                 
                 <<<<<<<<<< 1 定义结构体>>>>>>>>>>
                 typedef struct DelegateStruct {
                 unsigned int  callback;
                 
                 } DelegateType;
    
                 <<<<<<<<<< 2 声明结构体>>>>>>>>>>
                 @property (nonatomic, assign) DelegateType delegateType;
    
                 <<<<<<<<<< 3 重写delegate的setter>>>>>>>>>>
                 - (void)setDelegate:(id<LFAudioCaptureDelegate>)delegate {
                 
                         _delegate = delegate;
                        if (_delegate && [_delegate respondsToSelector:@selector(captureOutput:audioData:)]) {
                            _delegateType.callback = 1;
                            }
                 }
    
                 <<<<<<<<<< 4 根据缓冲判断>>>>>>>>>>
                 if (source.delegateType.callback == 1) {
                     [source.delegate captureOutput:source audioData:[NSData dataWithBytes:buffers.mBuffers[0].mData length:buffers.mBuffers[0].mDataByteSize]];
                 }
                 */
                
                
                if (source.delegate && [source.delegate respondsToSelector:@selector(captureOutput:audioData:)]) {
                    [source.delegate captureOutput:source audioData:[NSData dataWithBytes:buffers.mBuffers[0].mData length:buffers.mBuffers[0].mDataByteSize]];
                }
            }
            return status;
        }
    }
  • 相关阅读:
    oracle如何在所有procedure里搜索某些关键字, 存储过程
    Delphi 中文件的操作FileOpen
    【oracle】varchar和varchar2区别
    Delphi 2010 新增功能之: IOUtils 单元(6): TPath(结构体) 的方法与属性
    oracle如何在所有procedure里搜索某些关键字, 存储过程
    Delphi ADOConnection连接 sqlserver
    一种在SQLServer中实现Sequence的高效方法
    SQL Server 序列(SEQUENCE)使用
    [惠普HP] HP1215出现硒鼓底灰刮板拆机图解教程
    记录一下 山客 BK650 UPS 的配置软件下载地址
  • 原文地址:https://www.cnblogs.com/madaha/p/9687731.html
Copyright © 2020-2023  润新知