如何解决移动直播下的耳返延迟问题
耳返是主持人或歌手在一些大型晚会现场会佩戴的一种电子设备。耳返其实是“使用耳机形式的返送”,它是包括耳机、无线接收器、无线发射器,混音器等一系列设备的总称,与之对比的是“地返”,是指装在舞台前方固定的用来返送的音响。
对于歌手而言,演出现场往往是个很大的空间,会有声音的延迟或变质,当歌手在现场演唱时往往听不到自己的声音,或者听到伴奏时已发生延迟,所以需要耳返把伴奏送到歌手耳边以保证他们不会唱错拍子或走音走调。
一、耳返存在的问题
通过前面的介绍我们了解到,耳返的使用对延时性要求比较高,如果延时大于 30ms,人听到的耳返声音就会有“错位”的感觉,如果大于 50ms 就会明显感觉到延迟的存在。随着延迟的不断增大,对于演奏者来说,不但不能很好的通过耳返来及时调整演奏的节奏,甚至还会影响演奏的整体效果。
如上图,耳返延时也就是声音在设备上的往返延迟: 音频信号进入移动设备的输入组件,由应用处理器上运行的应用进行处理,然后从输出组件传出,这整个过程所花费的时间就是声音的完整往返时间。影响耳机的往返延迟主要表现在以下几方面:
1.移动设备中使用的扬声器和麦克风由于设备类型多样及质量参差不齐,在有些设备上通常音效较差,所以会通过增加信号处理功能来改善音质。此类信号处理会引起延迟。
2.对于同步输入和输出,每一侧将使用单独的缓冲区队列完成处理程序。即使两侧采用相同的采样率,也无法保证这些回调的相对顺序或音频时钟的同步。应用应当缓存数据,并适当进行缓冲区同步,使用的数据缓存会引起延迟。
3.在音频处理过程中由于音频采集和播放的采样率不统一或其他处理原因而进行的采样率转换,也会造成延迟。
4.数据第一次在缓冲区加入队列后启动音频管道所需时间的长短也会产生不同的延迟。
移动端对声音耳返的处理中,iOS 的表现明显优于 Android。Android 端延迟的问题主要表现在: 系统 API 在不同机型上延迟表现存在多样性,有些机型甚至会达到 300ms 的延迟,这样完全不能满足耳返的功能需求。
二、耳返实现的方式
1.通过 Android 提供系统 API 进行声音的采集和播放,其特点是使用简单。但由于是 Java 层的 API,无论是播放还是采集都需要将数据在 Java 层和 Native 层之间拷贝,影响性能且延迟较大。不同型号设备间,由于厂商对系统层不同的修改也会导致声音延迟表现差别较大,范围大致在 150ms-300ms 之间。
在 Android 设备上运行时,并不能确定通过任何路径的音频延迟时间,不过我们可以通过对下列硬件功能标记,来了解硬件设备是否能为延迟时间提供保证。
2.android.hardware.audio.low_latency 指示 45ms 或更短的持续输出延迟时间。
3.android.hardware.audio.pro 指示 20ms 或更短的持续往返延迟时间。
可以通过如下代码检测:
boolean hasLowLatencyFeature = getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY); boolean hasProFeature = getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO);
为了最大限度的缩短延迟时间,我们需要获取与设备最佳匹配的采样率和缓冲数据大小。
以下代码可以从 AudioManager 获得最佳采样率:
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); String sampleRateStr = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); int sampleRate = Integer.parseInt(sampleRateStr); if (sampleRate == 0) sampleRate = 44100; // Use a default value if property not found
以下代码可以从 AudioManager 获取采用与获得最佳采样率相似的方式获得最佳缓冲区大小。PROPERTY_OUTPUT_FRAMES_PER_BUFFER 属性表示 HAL(硬件抽象层)缓冲区可以容纳的音频帧数。如果使用正确数量的音频帧,会定期出现回调,而这将减少抖动。在不同的设备及不同的 Android build 中,HAL 缓冲区大小有所不同。
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); int framesPerBufferInt = Integer.parseInt(framesPerBuffer); if (framesPerBufferInt == 0) framesPerBufferInt = 256; // Use default
以上确定好设备的音频采集播放的最佳参数后,我们就可以实现音频的采集和播放功能了。在 Android 系统 API 中可以通过创建 AudioRecorder 和 AudioTrack 类实例来实现设备端的音频采集和音频播放。
// 采集 audioRecord =new AudioRecord(
audioSource, sampleRate, channelConfig, audioFormat, bufferSizeInBytes);
int bytesRead = audioRecord.read(byteBuffer, byteBuffer.capacity());
//播放
if (Build.VERSION.SDK_INT >= 21) {
audioTrack =
createAudioTrackOnLollipopOrHigher(sampleRate, channelConfig, minBufferSizeInBytes);
} else {
audioTrack =
createAudioTrackOnLowerThanLollipop(sampleRate, channelConfig, minBufferSizeInBytes);
}
}
2.通过 openSL ES,实现复杂,延迟较小,而且有可控的延迟 buffer 可调。不论是编写合成器、卡拉 OK、游戏还是其他实时应用,都可以使用 C 或 C++ 实现高性能、低延迟时间音频。而且 Android 在 O 版本中引入 AAudio 作为 OpenSL ES 库的轻量级原生 Android 替代项,专门针对低延时高性能的音频应用设计,实现起来相对 openSL ES 要简单许多,性能上也会相对更优。
对于声明 android.hardware.audio.low_latency 功能的设备, 也可以通过前面的方法获取设备的最佳采样率、缓冲区,应用与 OpenSL ES 中,已实现设备上更短的延迟时间。
opengGL 的创建流程:
a. 创建对象(通过带有 create 的函数)
b. 初始化(通过 Realize 函数)
c. 获取接口来使用相关功能(通过 GetInterface 函数)3.个别厂商提供的耳返接口: 有些手机厂商优化系统底层逻辑,针对 Android 系统的音频耳返功能优化出最理想的效果,如: 华为、vivo、oppo 会有厂商提供的专门的耳返 API 调用,这种方式实现的耳返效果延迟相对最优。
为了针对不同 Android 手机实现最优的延迟效果,融云采用厂商 + openSL ES + 系统 API 的方式联合实践。当手机支持厂商提供耳返 API 时,融云音视频 SDK 会优先使用厂商耳返功能,这样可以使这部分手机在耳返功能上表现出最好的效果。针对不支持耳返功能的手机设备,融云音视频 SDK 则采用 openSL ES 的方式实现耳返功能,使这部分手机可以通过 openSL ES 实现较好的耳返效果。由于 openSL ES 在复杂多样的 Android 系统设备上存在兼容问题,针对这种情况,融云音视频 SDK 会采用 Android 系统 API 提供耳返功能,以确保音视频 SDK 的稳定性。这样融云音视频 SDK 不仅能最大程度的降低 Android 设备的耳返延迟效果,而且可以最大限度的兼容不同的 Android 设备。