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