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

摘要:
每个element表示一个音频处理上下文,也称为bus。每个element有输出和输出部分,称为scope,分别是inputscope和Outputscope。代码分解@propertyAudioComponentInstancecomponetInstance;/*代表一个特定的音频组件对象*/@propertyAudioComponentcomponent;/*代表一个特定的音频组件类*/@propertydispatch_queue_ttaskQueue;@propertyBOOLisRunning;@propertyLFLiveAudioConfiguration*configuration;-initWithAudioConfiguration:configuration{if{_configuration=configuration;self.isRunning=NO;self.taskQueue=dispatch_queue_create;AVAudioSession*session=[AVAudioSessionsharedInstance];/*音频线路切换监听*/[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selectorname:AVAudioSessionRouteChangeNotificationobject:session];/*录音功能被打断监听*/[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selectorname:AVAudioSessionInterruptionNotificationobject:session];/*用于描述一个音频组件的独特性和识别ID的结构体*/AudioComponentDescriptionacd;/*音频组件主类型:输出类型*/acd.componentType=kAudioUnitType_Output;//acd.componentSubType=kAudioUnitSubType_VoiceProcessingIO;/*音频组件的子类型:RemoteIO,即从硬件采集和输出到硬件的audioUnit,它的逻辑是固定的:固定2个element,麦克风经过element1到APP,APP经element0到扬声器。

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到扬声器。

ios 实时音频流获取---分解LFLiveKit第1张ios 实时音频流获取---分解LFLiveKit第2张
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];
    }
    returnself;
}
- (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.delegaterespondsToSelector:@selector(captureOutput:audioData:)]) {
                [source.delegate captureOutput:source audioData:[NSData dataWithBytes:buffers.mBuffers[0].mData length:buffers.mBuffers[0].mDataByteSize]];
            }
        }
        returnstatus;
    }
}

免责声明:文章转载自《ios 实时音频流获取---分解LFLiveKit》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇C# WinForm开发系列 DataGrid/DataGridViewkotlin面向对象之抽象类、继承、多态下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

随便看看

kafka查询命令---Linux

Kafka版本:Kafka_2.12-2.1.1kafka _端口默认为9092,zk_端口默认值2181查看topicbin/Kafka主题。sh--zookeperzk_ip:zk_Port--list查看groupbin/kafka用户组。sh--引导服务器kafka-ip:kafka_Port--list查看主题下的基础...

canvas基础绘制矩形(1)

1.画布基础知识画布元素是HTML5中添加的一个重要元素,专门用于绘制图形。然而,画布本身不具备绘制图形的能力。将画布元素放置在页面上相当于在页面上放置矩形“画布”。我们可以使用js脚本在“画布”上绘制图形。...

Foxyproxy 火狐代理插件

Firefox上的插件Autoproxy一直很难使用。它永远不能更新规则,但foxyproxy可以替代它。用鼠标中键单击foxyproxy图标以在不同的代理方法之间切换。foxyproxy图标从foxhead变为蓝色,因为内容传输发生在网页中,该传输通过默认代理服务器,默认代理的初始颜色为蓝色。...

如何快速把ps序列图层建立帧动画?

工具ps1。将序列帧图片加载到ps新建-˃脚本-˃将文件加载到堆栈2中。创建序列帧动画窗口-˃时间线-˃时间线面板的右上菜单-˃从层3创建帧。移除多余的透明画布选择所有层-˃图像-˃剪辑-˃基于透明度4。将图层保存到图片文件-˃脚本-˃将图层保存为文件隐藏白色背景5。导出序列框架文件-˃自动-˃联系人表6存储为png...

Navicat数据存放位置和备份数据库路径设置

navicat数据库存储在哪里?有了这样的问题,让我们来解决这个问题。默认情况下安装Navicat,默认情况下也安装MySQL,数据库存储在默认用户的目录中。选择安装目录时,还可以选择数据的位置。很多人此时只是设置了MySQL的安装位置。...

搭建我的世界服务器(史上最详细) java环境配置 ,免费内网穿透,家庭用电脑也欧克

服务器部署周末想要和好基友联机?这里有最简单的开服教程!最后打开我的世界输入服务器ip,和你自己在内网穿透网站设置的端口连接即可成功要想服务器稳定运行,要保证命令窗口和端口映射一直开着...