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