前言

视频直播,K歌应用等等都会有音频录制的功能,音频录制时还可以带有耳返效果,那这些是如何实现的呢?如果仅仅是录制音频,那使用IOS的AudioQueue框架实现即可,但是在直播这些实时性要求比较高、特效比较多(比如混音,变声等)的应用中,AudioQueue可能满足不了要求了,AudioUnit可以完成这些功能。
本文将介绍用AudioUnit完成音频采集,耳返效果,保存裸PCM音频文件

AudioUnit音频系列

AudioUnit之-播放裸PCM音频文件(一)
AudioUnit之-录制音频+耳返(二)
AudioUnit之-录制音频保存为m4a/CAF/WAV文件和播放m4a/CAF/WAV文件(三)
AudioUnit之-录制音频并添加背景音乐(四)
AudioUnit之-generic output(离线)混合音频文件(五)

实现思路

先看一张图片,该图片来自官方关于AudioUnit Augraph的说明文档中,官网文档

IO_unit_2x.png

1、图解
这里的Remote I/O Unit代表了扬声器和麦克风硬件,其中Element0代表了扬声器,Element1代表了麦克风。Element0(扬声器)的Input scope连接着app端,Output scope连接着扬声器硬解,Element1(麦克风)的Input scope连接着麦克风硬件端,Output scope连接着app端。
2、播放音频过程
系统定时从播放缓冲区(该缓冲区对app不可见)中读取音频数据输送给扬声器硬件,扬声器进行播放,所以只要app不停的往该播放缓冲区中输送音频数据(即通过Element0的Input scope输送数据),保证该缓冲区中有音频数据,那么音频将持续播放,至于用AudioUnit播放PCM音频的代码可参考前面的文章AudioUnit播放PCM音频
3、采集过程
采集过程则是播放过程的逆过程,麦克风不停的采集数据放入采集缓冲区(该缓冲区对app不可见),所以只要app不停的从该采集缓冲区中读取音频数据(即通过Element1的Out scope获取数据),那么即可完成音频的采集过程,该音频数据为裸PCM音频数据,你可以直接保存到文件中,也可以用aac编码后保存到m4a文件中,也可以直接发送到网络中等等。
4、耳返实现
所谓耳返,就是录制的音频实时输送回扬声器进行播放。有了前面2、3的分析,现在来看耳返的实现是不是很简单了,没错,就是app从Element1的Out scope获取的音频数据再通过Element0的In scope输送出去即完成了耳返功能

音频数据结构AudioBufferList解析

这个数据结构对音频存储很重要,有必要弄清楚

对于packet数据,各个声道数据依次存储在mBuffers[0]中,对于planner格式,每个声道数据分别存储在mBuffers[0],...,mBuffers[i]中
对于packet数据,AudioBuffer中mNumberChannels数目等于channels数目,对于planner则始终等于1
kAudioFormatFlagIsNonInterleaved对应的是planner格式;kAudioFormatFlagIsPacked对应的则是packet格式

struct AudioBufferList
{UInt32      mNumberBuffers;AudioBuffer mBuffers[1]; // this is a variable length array of mNumberBuffers elements#if defined(__cplusplus) && defined(CA_STRICT) && CA_STRICT
public:AudioBufferList() {}
private://  Copying and assigning a variable length struct is problematic; generate a compile error.AudioBufferList(const AudioBufferList&);AudioBufferList&    operator=(const AudioBufferList&);
#endif};
typedef struct AudioBufferList  AudioBufferList;

它的创建代码如下:

_bufferList = (AudioBufferList *)malloc(sizeof(AudioBufferList) + (chs - 1) * sizeof(AudioBuffer));_bufferList->mNumberBuffers = _isPlanner?(UInt32)chs:1;for (NSInteger i=0; i<chs; i++) {_bufferList->mBuffers[i].mData = malloc(BufferList_cache_size);_bufferList->mBuffers[i].mDataByteSize = BufferList_cache_size;}

录制音频实现代码

这里只贴出部分关键代码,详细代码请参考后面Demo。
1、准备工作
配置正确的音频会话

 //  2、======配置音频会话 ======///** 配置使用的音频硬件:*  AVAudioSessionCategoryPlayback:只是进行音频的播放(只使用听的硬件,比如手机内置喇叭,或者通过耳机)*  AVAudioSessionCategoryRecord:只是采集音频(只录,比如手机内置麦克风)*  AVAudioSessionCategoryPlayAndRecord:一边采集一遍播放(听和录同时用)*/[_aSession setCategory:category error:nil];

前面的文章我们播放音频,使用的是AVAudioSessionCategoryPlayback,如果要录制那么要使用AVAudioSessionCategoryRecord,如果要实现耳返就要使用AVAudioSessionCategoryPlayAndRecord。
tips:
其实我们使用一种即可AVAudioSessionCategoryPlayAndRecord,这样录制和播放都可以使用了
2、创建RemoteIO Unit

// 创建RemoteIO_iodes = [ADUnitTool comDesWithType:kAudioUnitType_Output subType:kAudioUnitSubType_RemoteIO fucture:kAudioUnitManufacturer_Apple];

3、开启录制功能
重要,扬声器默认是开启的,麦克风默认是关闭的
// 1、开启麦克风录制功能
UInt32 flag = 1;
OSStatus status = noErr;
// 对于麦克风:第三个参数麦克风为kAudioUnitScope_Input, 第四个参数为1
// 对于扬声器:第三个参数麦克风为kAudioUnitScope_Output,第四个参数为0
// 其它参数都一样;扬声器默认是打开的
status = AudioUnitSetProperty(_ioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &flag, sizeof(flag));
if (status != noErr) {
NSLog(@"AudioUnitSetProperty kAudioUnitScope_Output fail %d",status);
}
这里解释下为什么AudioUnitSetProperty()函数第三个参数对应的是kAudioUnitScope_Input,前面不是说采集音频时是从Element1的output scope读取数据吗,应该是kAudioUnitScope_Output才对吧。很容易被绕混了,其实这样理解,开启录制功能,对应的是系统麦克风硬件,所以应该是Input scope。
4、设置采集的音频数据输出格式
重要,app从采集缓冲区读取的数据格式要指定,否则app如何处理这个数据呢,对吧,比如采样率,声道数,采样格式等等。

// 录制音频的输出的数据格式CGFloat rate = self.audioSession.currentSampleRate;NSInteger chs = self.audioSession.currentChannels;AudioStreamBasicDescription recordASDB = [ADUnitTool streamDesWithLinearPCMformat:flags sampleRate:rate channels:chs bytesPerChannel:_bytesPerchannel];// 设置录制音频的输数据格式status = AudioUnitSetProperty(_ioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &recordASDB, sizeof(recordASDB));if (status != noErr) {NSLog(@"AudioUnitSetProperty _ioUnit kAudioUnitScope_Output fail %d",status);}

这里又是kAudioUnitScope_Output了,有人可能会问,采集的数据格式不应该是在input scope端吗?NO,NO,NO,系统将麦克风采集的数据经过转换后放在采集缓冲区中,这里设置的就是转换后放在采集缓冲区的数据格式,所以是kAudioUnitScope_Output
5、设置采集输出的回调
这里也是重要的一步,系统将采集的音频数据放入采集缓冲区,那app如何知道缓冲区是否有数据呢?系统有一个回调函数,该回调函数被调用了说明缓冲区中有数据,所以我们只要设置该回调函数即可

AURenderCallbackStruct callback;
callback.inputProc = saveOutputCallback;
callback.inputProcRefCon = (__bridge void*)self;
/** tips:前面即使将麦克风的输出作为扬声器的输入,这里也可以再为麦克风的输出设置回调,他们是互不干扰的。但是需要在回调里面手动调用*  AudioUnitRender()函数将数据渲染出来*/
AudioUnitSetProperty(_ioUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Output, 1, &callback, sizeof(callback));

那么当采集缓冲区中有数据的时候,saveOutputCallback将被调用,下面看一下该回调函数长撒样
tatic OSStatus saveOutputCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData)
{
.........
}
6、从采集缓冲区获取音频数据
第5 步app知道缓冲区中有数据了,那app如何拿到该数据呢?细心的人可能都看到前面的备注了,对的,就是通过AudioUnitRender()函数来获取数据,如下就是获取数据的代码

status = AudioUnitRender(player->ioUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, bufferList);
最终音频数据将按照前面第四步中指定的方式存储于bufferList中
第一个参数代表 Remote I/O Unit
第二,三,四,五个参数没什么好解释的,传saveOutputCallback对应的即可,告诉系统,app要指定时间,指定书目的音频数据,不要乱回数据^
^。
第6个参数要注意下,它必须和前面第4步指定的输出格式对应,比如如果前面第四步是kAudioFormatFlagIsNonInterleaved,那么这里的buffList的mNumberBuffers就为声道数目
bufferList的具体创建逻辑为:

_bufferList = (AudioBufferList *)malloc(sizeof(AudioBufferList) + (chs - 1) * sizeof(AudioBuffer));_bufferList->mNumberBuffers = _isPlanner?(UInt32)chs:1;for (NSInteger i=0; i<chs; i++) {_bufferList->mBuffers[i].mData = malloc(BufferList_cache_size);_bufferList->mBuffers[i].mDataByteSize = BufferList_cache_size;}

7、保存录制的音频
第6步拿到的音频数据放在了AudioBufferList结构体定义的结构中,它是裸的音频数据,对于planner格式和packet格式音频数据,它的存储方式也是不一样的,所以我们处理时也要分别对待。
这里以直接将裸音频数据保存到文件中为例说一下保存的区别

if (isPlanner) {// 则需要重新排序一下,将音频数据存储为packet 格式int singleChanelLen = bufferList->mBuffers[0].mDataByteSize;size_t totalLen = singleChanelLen * chs;Byte *buf = (Byte *)malloc(singleChanelLen * chs);bzero(buf, totalLen);for (int j=0; j<singleChanelLen/bytesPerChannel;j++) {for (int i=0; i<chs; i++) {Byte *buffer = bufferList->mBuffers[i].mData;memcpy(buf+j*chs*bytesPerChannel+bytesPerChannel*i, buffer+j*bytesPerChannel, bytesPerChannel);}}if (player.dataWriteForPCM) {[player.dataWriteForPCM writeDataBytes:buf len:totalLen];}// 释放资源free(buf);buf = NULL;} else {AudioBuffer buffer = bufferList->mBuffers[0];UInt32 bufferLenght = bufferList->mBuffers[0].mDataByteSize;if (player.dataWriteForPCM) {[player.dataWriteForPCM writeDataBytes:buffer.mData len:bufferLenght];}}

通过上面代码,我们看到对于planner格式,我们还要先将planner格式转换成packet格式,然后再写入文件。为什么呢?因为裸音频数据在文件中是以packet格式存放的。

耳返实现代码

上面讲解了如何录制音频,并且将录制的裸音频数据保存到文件中。那如何开启耳返功能呢?其实只需在上面步骤中增加一个步骤即可,看下代码
AUGraphConnectNodeInput(_augraph, _ioNode, 1, _ioNode, 0);
解释一下这句代码,第一二四个参数没什么好说的,主要第三个和第五个参数,它代表将麦克风的输出连接到扬声器的输入,即当采集缓冲区有数据时系统自动为我们将音频数据输出到扬声器的Element0的Input scope,那我们在录制音频的同时就可以听到我们自己的声音了(该延迟非常低,基本感觉不出来)。
有人可能会问,那我们前面第五步设置的采集音频的回调还会调用吗?答案是会的,不影响。

项目地址

Demo
对应运行Demo截图

1563099390571.jpg

对应代码位置截图

1563099465040.jpg

1563099470971.jpg

AudioUnit录制音频+耳返(四)相关推荐

  1. Android音频子系统(十一)------耳机返听(耳返)原理实现

    你好!这里是风筝的博客, 欢迎和我一起交流. 耳返,也就是耳机返听,一般用在演唱会直播.手机K歌.KTV等场景. 例如在嘈杂的演唱环境里,通过佩戴耳返,歌手能清楚地听到伴奏和自己的声音,来鉴定自己有没 ...

  2. 如何解决移动直播下的耳返延迟问题

    ​​耳返是主持人或歌手在一些大型晚会现场会佩戴的一种电子设备.耳返其实是"使用耳机形式的返送",它是包括耳机.无线接收器.无线发射器,混音器等一系列设备的总称,与之对比的是&quo ...

  3. iOS开发 AudioUnit+AUGraph实现录音耳返功能

    文章目录 前言 需求分析 使用AudioUnit的原因 使用AUGraph的原因 具体实现步骤 GSNAudioUnitGraphDemo使用方法 出现过的问题及思考 后记 Demo地址 参考文章 前 ...

  4. 耳机插在电脑上怎么录音,在线录制音频的软件有什么?

    在公共场合,很多人在听语音或者其他有声音的音频时,都会选择戴上耳机,不仅仅是因为环境需要,而且戴上耳机会听的更加清楚,那就有很多人有疑问了,为什么将耳机插到电脑上没有办法录音了,可能是因为电脑没有设置 ...

  5. android 耳返解决方案,vivo又出招手机音频领域:联合全民K歌发布首个安卓实时耳返方案...

    原标题:vivo又出招手机音频领域:联合全民K歌发布首个安卓实时耳返方案 通常我们到vivo手机,一般大家都会都会说的是其拍照方面的表现,其实并不然,vivo对于音乐方面也是非常重视的,可以vivo的 ...

  6. Zego SDK深度适配安卓机型,实现超低延迟耳返

    耳返功能又称耳机返听,耳机采集监听,在设备上插入耳机(普通耳机或蓝牙耳机),能从耳机侧听到麦克风采集的声音. 该功能可以使主播在K歌直播.K歌歌曲录制.个人清唱,朗诵等场景下实时监听自己的声音,让观众 ...

  7. Flutter耳返和双声道功能的实现

    1 耳返功能简介 ZEGO Express SDK 提供了Flutter耳返和双声道的功能,在视频直播.K歌.音频录制等场景下广泛应用,开发者可根据实际业务场景需要设置,一套代码可实现跨平台音视频耳返 ...

  8. Android 耳返实践 OpenSL ES AAudio Oboe

    耳返概述: 耳返主要实现监听的功能,在低延时的情况下可以给主播一个比较真实音频的反馈,在演唱会等专业场景里比较常用. 技术实现上来说就是要时时的把录制进的音频数据立刻播放出去,当然这个过程要低延迟. ...

  9. Android录制音频的三种方式

    对于录制音频,Android系统就都自带了一个小小的应用,可是使用起来可能不是特别的灵活.所以有提供了另外的俩种. 下边来介绍下这三种录制的方式; 1.通过Intent调用系统的录音器功能,然后在录制 ...

最新文章

  1. MongoDB管理: 使用killOp干掉Long Running Operation
  2. Pycharm 基本快捷键
  3. linux机群下NFS+NIS服务的搭建以及MPICH的简单安装
  4. java-IO-基本输出输入流
  5. oracle sql语句
  6. oracle 数据库中数据导出到excel
  7. 5菜鸟教程_excel图文教程:应用PQ工具进行数据整理
  8. WPF 中设置Combox下拉框Text 显示值
  9. 滴滴回应上班高峰期大范围崩溃 :系统异常 订单差额部分统一退还
  10. Spring Boot学习总结(3)——SpringBoot魅力所在
  11. Direct3D光与材质的颜色值
  12. 前端面试宝典(2)——JavaScript
  13. IC卡读写模块(MFRC522) 简介调试QT实现
  14. 百度地图3.1教程—检索功能演示
  15. iOS开发脚踏实地学习day14-绘图
  16. 软件部件仿真测试平台的设计与实现(计算机工程与设计2017-11)
  17. Orange's:一个操作系统的实现 Descriptor 3宏详解
  18. 抱歉,我也不知道程序员35岁以后该怎么办!
  19. php 函数索引 中文索引
  20. (附源码)springboot毕业论文管理系统 毕业设计 030946

热门文章

  1. 雪峰磁针石博客]渗透测试简介2入侵工具
  2. 化合物筛选“杀手锏”:高质量化合物库+一站式虚拟筛选,这里都备齐了
  3. 先进后出的数据结构-栈 一
  4. 算法分析与设计期末总结
  5. 计算机专业大一期末总结
  6. 上传文件删除上传文件——前端layui
  7. Win10 环境下配置 Docker + Laradock + Laravel
  8. 合肥市专利申请费用减缓流程是怎样的
  9. 刷爆全网的动态条形图,原来5行Python代码就能实现!
  10. BACK-OFF RESTARTING FAILED CONTAINER 的解决方法