前言

视频直播,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. Python全栈之路Day13
  2. 牛客网 【每日一题】5月13日 加分二叉树
  3. Ajax和Jsonp实践
  4. PAT乙类之1011 A+B 和 C
  5. 波士顿动力放出新视频:谁都挡不住机器狗开你的门
  6. Android UI(四)云通讯录项目之云端更新进度条实现
  7. [UE4.4.x] 虚幻4 安卓打包
  8. python冒泡排序时间复杂度_Python算法中的时间复杂度问题
  9. LeetCode 106. Construct Binary Tree from Inorder and Postorder Traversal
  10. 计算机网络的发展英文,计算机网络英文方向论文选题 计算机网络英文论文题目哪个好...
  11. Kafka集成Kerberos之后如何使用生产者消费者命令
  12. debian解决中文乱码,安装chinese font
  13. paip.输入法英文词库的处理 python 代码 o4
  14. 乐优商城(一)项目介绍
  15. MT6763芯片资料MT6763处理器性能介绍
  16. 数据结构之平衡二叉树详解
  17. android toast 大小,Android Toast实现全屏显示
  18. 新建word文档如何删掉页眉横线
  19. 用计算机求和公式,全国计算机一级等考excel之sum求和函数
  20. 程序员今年最值得关注的 23 种新移动技术

热门文章

  1. PHP跨域问题解决方案
  2. 卷积的意义(幽默解释)--转
  3. 战区与游戏服务器的连线已中断,使命召唤战区无法连接服务器如何解决?无法连接服务器解决方法介绍[多图]...
  4. Python pandas 读取csv/txt数据文件 python读取csv/txt文件
  5. 中字符转换中文乱码的处理方法
  6. 留学人员入户佛山需要哪些材料
  7. 用金字塔原理看“⑧荣⑧耻”
  8. 查看docker与宿主机网卡的对应关系,tcpdump在容器外网络抓包
  9. 10的100次方是哪个公司
  10. 解决标题党的计算机算法用户app,今日头条算法工程师:做好推荐必须打击标题党...