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;
    }
}

 

 

 

转载于:https://www.cnblogs.com/madaha/p/9687731.html

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值