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

你好!这里是风筝的博客,

欢迎和我一起交流。


耳返,也就是耳机返听,一般用在演唱会直播、手机K歌、KTV等场景。
例如在嘈杂的演唱环境里,通过佩戴耳返,歌手能清楚地听到伴奏和自己的声音,来鉴定自己有没有走音。
或者打开手机大部分K歌软件都会有耳返功能,实时监听自己的声音,不断调整声音状态,呈现最佳的视听效果。

耳返常见的两种实现方式:

  • 在HAL层实现:在HAL拿到mic数据,新建一个线程用ringbuf把数据存起来,和下行音乐数据做混音,然后再往kernel底层写数据播放。
  • 在AF实现:录音数据上报到AudioFlinger,拿到录音之后直接和音乐一起播放。

两种方法各有利弊:在HAL实现的话需要考虑不同采样率之间的混音问题。在AF实现,多了一层通路会导致耳返延时变大(150ms-300ms)。

一般我更喜欢在HAL实现,因为延时是耳返很重要的一个指标。(耳返延时是指声音在设备上的往返延迟: 音频信号从mic进入应用处理,再由输出设备输出,这整个过程所花费的时间就是声音的完整往返时间。)

之前在芯片原厂的时候,也有个需求,客户需要一个低延时耳返功能,要求延时在30ms之内,主要用户KTV场景,需要我们芯片原厂提供方案。

一般人耳对声音不能分辨出 30ms之内的差值,如果延时大于 30ms,人听到的耳返声音就会有“错位”的感觉,如果大于 50ms 就会明显感觉到延迟的存在。

老东家的芯片是支持硬件混音的,可以节省掉一点时间。当时也是尝试在HAL层去做,因为虽然硬件支持混音,但是没有方法在芯片内部做到mic loopback spk,所以还得在HAL里起线程,将mic数据送到playback去才能在底层混音。

当时实测延时在100ms左右,其实我们知道,延时是和buffer size相关的,buf大的话延时势必会高。
当时默认period size 1024,period count 4,修改playback为 480 x 4、record为 240 x 2 之后(对应两倍的关系,具体参数不太记得了,大概是这样),延时成功降低到了25ms,终于达到了要求!

但是会有一个很致命的问题,buffer size太小会有xrun的风险,导致播放出现overrun,录音出现underrun,造成卡顿和杂音…

方案比较粗糙,不过当时也没有其他方案了。

但是来到手机厂之后,因为手机上面主要是做K歌的耳机返听,居然发现了更牛逼的实现,太猛了!

这里主要以全民K歌为例,在全民K歌开启耳机返听的情况下,因为设置了fast flag,所以会用比较小的buffer size进行数据传输,这样可以保证更好的实时性、低延时。
但是缺点我上面也提过了,容易出现xrun!

高通的话是在ADSP内部搭建通路,这部分不太了解不做评价。

这里主要探讨没有ADSP的方案:从Android到Linux底层这一个音频流程中寻找切入点,将上行人声和下行音乐混音,建立耳返通路,并减小这一个耳返通路的buffer size以提高实时性的要求。

hal

如果还像之前的方案一样的话,在HAL做mix,因为在混音后的下行通路上仍存在由ALSA管理的ring buffer作为Audio out线程和芯片底层中断交互的缓存,这一个缓存就是播放通路本身的缓存,这一个缓存大小对应的声音时长接近120ms,这样的延时是明显不能满足低延时的要求的,但是又不能将buffer size减小,否则用户在用手机播放时必然会受到影响,有卡顿、杂音的风险,这是厂商不能承受的。

此时就出现了一个难点:耳返的人声必须要使用小的buffer size保证实时性,而播放音乐使用的是大buffer size且不能随意调整大小,耳返方案必须完全满足以上两点需求。

所以,我们必须在播放流程的更”下游”找到一个小buffer size作为混音切入点。
但是从流程上看,ALSA管理的这一个ring buffer已经是最底层的一个buffer,这一个buffer的大小对应的声音时长有120ms,我们若需要切入一个小buffer混音,则必须将这一个ALSA管理的ring buffer进行分割,化整为零。

这一个ring buffer的读写机制并没有特别之处,在播放的场合,AudioOut线程作为供应者不断写入数据,写指针不断增加,增加的大小达到buffer总大小后就复位,重新开始覆盖原来的数据进行写入;而底层芯片中断则作为消费者,不断消耗数据,读指针不断增加,增加的大小达到buffer总大小后就复位,保持处于写指针的后方,不断跟随着写指针。

这样我们就可以有一个大胆的想法:将人声混入写指针与读指针之间的buffer,混入点的指针位置与底层中断的读指针越接近,实时性就必然越高。方案的实现原理如下图:
mix
把ring buffer拉直来看:
buf
其中offset就是混入点的指针位置与底层中断的读指针之间的差距,也即是延时对应的buffer,也就是人声+背景音乐混音后的声音数据。

至此,完整方案就出来了,兼容了以小buffer size播放低延时人声和以原有的大buffer播放高延时音乐这两点需求。

其实在Android O上面,引入AAUDIO专门针对低延时高性能的音频应用设计(在以前的系统版本中只能使用OpenSL ES),这是官网的介绍:
https://developer.android.com/ndk/guides/audio/aaudio/aaudio
在HAL里主要靠mmap来实现,不过我也没有实际用过,我看有贴子说:在Android 9.0上调用AAudio API的Pixel 2 XL测试结果如图,14ms

不过也有两个难点:
难点一:Android 系统自带的耳返功能较为单一,不能满足用户多样化的需求。
难点二:Android系统繁杂,型号多样,大部分手机设备厂商直接使用Android系统的接口,硬件支持程度的不同,导致耳返效果不同,一些机型耳返延迟(从系统采集到人声从耳机输出)较高。

所以为了针对不同 Android 手机实现最优的延迟效果,各个手机厂商都是自己优化系统底层逻辑,针对 Android 系统的音频耳返功能优化出最理想的效果,自己实现耳返功能。

网上这块倒是没有看到有其他厂家是如何实现的,看到有个博主po有一张图:
huawei
华为K歌耳返方案提供双流的底回环通路,录音数据到达HAL层之后,一部分数据送给上层APP,另外一部分数据送给HIFI模块,HIFI模块混音特效加持后直接送给用户,而APP只要实现伴奏的播放即可。同时,在新平台对底层又进行深度优化,底层回环耳返延迟(见上图绿色箭头)可达到30ms以下。

这部分是不是真的就不知道了,有懂哥的话可以留言讨论下~


AF实现可以参考:Android耳返功能的实现

猜你喜欢

转载自blog.csdn.net/Guet_Kite/article/details/126853385