x86 移动设备上的 Android 低延迟音频





5.00/5 (1投票)
本文档介绍了自基于Intel® Atom™处理器(代号Bay Trail)的平台以来,x86设备上Android低延迟音频的实现方式。
Intel® Developer Zone提供跨平台应用开发工具和操作指南、平台和技术信息、代码示例以及同行专业知识,帮助开发人员创新并取得成功。加入我们的Android、物联网、Intel® RealSense™技术和Windows社区,下载工具、获取开发套件、与志同道合的开发人员交流想法,并参与黑客松、竞赛、路演和本地活动。
目标
本文档介绍了自基于Intel® Atom™处理器(代号Bay Trail)的平台以来,x86设备上Android*低延迟音频的实现方式。您可以使用本指南来帮助您研究在Intel®设备上使用低延迟Android版本(4.4.4)进行低延迟音频开发的方法。
注意:Android M Release音频仍在研究中。
引言
长期以来,Android在为专注于声音创作的应用提供低延迟音频解决方案方面一直不够成功。高延迟会对音乐创作、游戏、DJ和卡拉OK应用产生负面影响。这些应用中的用户交互会产生声音,而最终用户发现可闻信号的延迟过高,从而对其用户体验产生负面影响。
延迟是指从创建音频信号到通过某种交互进行播放所产生的延迟。往返延迟(Round-Trip Latency,简称RTL)是从系统或用户发出信号的输入操作到生成出站信号所需的时间延迟。
当用户触摸对象以生成声音,而声音在输出到扬声器之前延迟一段时间时,用户会在Android应用程序中体验到音频延迟。在大多数ARM*和x86设备上,音频RTL的测量值可能低至300毫秒,高至600毫秒,这主要发生在使用此处找到的Android音频开发方法的应用程序中:为减少延迟而设计。这些范围是用户无法接受的。期望的延迟必须远低于100毫秒,在大多数情况下,低于20毫秒是最理想的RTL。此外,还必须考虑Android在触摸式音乐应用中产生的总体延迟,即触摸延迟、音频处理延迟和缓冲区排队的总和。
本文档仅关注降低音频延迟,而不是总体延迟;然而,它确实考虑了总体延迟的大部分。
Android音频设计
Android的音频硬件抽象层(HAL)将高级、特定于音频的框架API(位于android.media中)连接到底层的音频驱动程序和硬件。
您可以在此处看到音频框架的图示:https://aosp.org.cn/devices/audio/index.html
OpenSL ES*
Android指定使用OpenSL ES API来开发最高效处理往返音频的稳健方法。尽管它不是低延迟音频支持的最佳选择,但它是推荐的选择。这主要是由于OpenSL利用的缓冲区排队机制,使其在Android媒体框架中更加高效。由于它是原生代码实现,因此可以提供更好的性能,因为原生代码不受Java*或Dalvik VM开销的影响。我们认为这是Android上音频开发的未来方向。正如Android原生开发工具包(NDK)关于Open SL的文档中所指定的,Android版本将继续改进Open SL的实现。
本文档将探讨通过NDK使用OpenSL ES API。作为OpenSL的介绍,请查看构成Android使用OpenSL进行音频代码库的三个层。
- 顶层应用程序编程环境是基于Java的Android SDK。
- 较低级别的编程环境,称为NDK,允许开发人员编写C或C++代码,这些代码可以通过Java Native Interface (JNI) 在应用程序中使用。
- OpenSL ES API,自Android 2.3起实现,内置于NDK中。
OpenSL像其他API一样,通过使用回调机制来工作。在OpenSL中,回调仅用于通知应用程序可以排队新的缓冲区(用于播放或录制)。在其他API中,回调还处理指向要填充或消耗的音频缓冲区的指针。但在OpenSL中,根据选择,API可以实现为回调作为信号机制,将所有处理保留在音频处理线程中。这包括在收到分配的信号后排队所需的缓冲区。
Google建议在OpenSL中使用一种称为Sched_FIFO策略的方法。Sched_FIFO策略基于环形或循环缓冲区技术。
Sched_FIFO策略
由于Android基于Linux*,Android会采用Linux CFS调度程序。CFS可能会以意想不到的方式分配CPU资源。例如,它可能会将CPU从具有较低nice值的线程中移走,转移到具有较高nice值的线程上。在音频的情况下,这可能会导致缓冲区定时问题。
主要解决方案是避免在高性能音频线程中使用CFS,并使用SCHED_FIFO
调度策略,而不是CFS实现的SCHED_NORMAL
(也称为SCHED_OTHER
)调度策略。
调度延迟
调度延迟是指线程变为就绪状态到完成上下文切换并实际在CPU上运行之间的时间。延迟越短越好,任何超过两毫秒的情况都会导致音频问题。长时间的调度延迟最有可能发生在模式转换期间,例如启动或关闭CPU、在安全内核和普通内核之间切换、从全功率模式切换到低功率模式,或调整CPU时钟频率和电压。
循环缓冲区接口
要测试缓冲区是否正确实现,第一步是准备一个代码可以使用的循环缓冲区接口。您需要四个函数:1) 创建一个循环缓冲区,2) 写入它,3) 从它读取,4) 销毁它。
代码示例
circular_buffer* create_circular_buffer(int bytes); int read_circular_buffer_bytes(circular_buffer *p, char *out, int bytes); int write_circular_buffer_bytes(circular_buffer *p, const char *in, int bytes); void free_circular_buffer (circular_buffer *p);
预期效果是,读取操作将仅读取缓冲区中已写入的数量,最多不超过请求的字节数。写入函数将仅写入缓冲区中有空间的字节。它们将返回读取/写入的字节数,该数字可以是零到请求的数量。
消费者线程(播放时为音频I/O回调,录制时为音频处理线程)从循环缓冲区读取数据,然后对音频进行处理。同时,异步地,生产者线程填充循环缓冲区,只有在缓冲区已满时才停止。通过适当的循环缓冲区大小,两个线程将无缝协作。
音频I/O
使用前面示例创建的接口,可以编写音频I/O函数来使用OpenSL回调。输入流I/O函数的一个示例如下:
// this callback handler is called every time a buffer finishes recording void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context) { OPENSL_STREAM *p = (OPENSL_STREAM *) context; int bytes = p->inBufSamples*sizeof(short); write_circular_buffer_bytes(p->inrb, (char *) p->recBuffer,bytes); (*p->recorderBufferQueue)->Enqueue(p->recorderBufferQueue,p->recBuffer,bytes); } // gets a buffer of size samples from the device int android_AudioIn(OPENSL_STREAM *p,float *buffer,int size){ short *inBuffer; int i, bytes = size*sizeof(short); if(p == NULL || p->inBufSamples == 0) return 0; bytes = read_circular_buffer_bytes(p->inrb, (char *)p->inputBuffer,bytes); size = bytes/sizeof(short); for(i=0; i < size; i++){ buffer[i] = (float) p->inputBuffer[i]*CONVMYFLT; } if(p->outchannels == 0) p->time += (double) size/(p->sr*p->inchannels); return size; }
在回调函数(第2-8行)中,该函数在每个新填充的缓冲区(recBuffer)准备好时被调用,所有数据被写入循环缓冲区。然后recBuffer可以再次排队(第7行)。音频处理函数(第10-21行)尝试读取请求的字节数(第14行)到inputBuffer,然后将这些样本数复制到输出(将其转换为浮点样本)。该函数报告复制的样本数。
输出函数
// puts a buffer of size samples to the device</pre> int android_AudioOut(OPENSL_STREAM *p, float *buffer,int size){ short *outBuffer, *inBuffer; int i, bytes = size*sizeof(short); if(p == NULL || p->outBufSamples == 0) return 0; for(i=0; i < size; i++){ p->outputBuffer[i] = (short) (buffer[i]*CONV16BIT); } bytes = write_circular_buffer_bytes(p->outrb, (char *) p->outputBuffer,bytes); p->time += (double) size/(p->sr*p->outchannels); return bytes/sizeof(short); } // this callback handler is called every time a buffer finishes playing void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) { OPENSL_STREAM *p = (OPENSL_STREAM *) context; int bytes = p->outBufSamples*sizeof(short); read_circular_buffer_bytes(p->outrb, (char *) p->playBuffer,bytes); (*p->bqPlayerBufferQueue)->Enqueue(p->bqPlayerBufferQueue,p->playBuffer,bytes); }
音频处理函数(第2-13行)接收一定数量的浮点样本,将其转换为短整型,然后将整个outputBuffer写入循环缓冲区,报告写入的样本数。OpenSL回调(第16-22行)读取所有样本并将其排队。
为了使此功能正常工作,需要将从输入读取的样本数与缓冲区一起传递给输出。以下是处理循环代码,将输入循环回输出:
while(on) samps = android_AudioIn(p,inbuffer,VECSAMPS_MONO); for(i = 0, j=0; i < samps; i++, j+=2) outbuffer[j] = outbuffer[j+1] = inbuffer[i]; android_AudioOut(p,outbuffer,samps*2); }
在此片段中,第5-6行循环遍历读取的样本并将它们复制到输出通道。这是一个立体声输出/单声道输入设置,因此输入样本被复制到两个连续的输出缓冲区位置。现在排队发生在OpenSL线程中,为了启动回调机制,在设备上启动音频后,我们需要排队一个用于录制和一个用于播放的缓冲区。这将确保在需要替换缓冲区时发出回调。
这是一个如何实现音频I/O跟踪线程以通过OpenSL进行处理的示例。每个实现都是唯一的,需要修改HAL和ALSA驱动程序才能充分利用OpenSL的实现。
x86 Android音频设计
OpenSL的实现并不保证所有设备都能以理想的延迟速率(低于40毫秒)进入Android“快速混音器”的低延迟路径。然而,通过修改媒体服务器、HAL和ALSA驱动程序,不同的设备可以在低延迟音频方面取得不同程度的成功。在研究在Android上降低延迟所需条件时,Intel在Dell* Venue 8 7460平板电脑上实现了一个低延迟音频解决方案。
实验结果是一个混合媒体处理引擎,其中输入处理线程由一个独立的低延迟输入服务器管理,该服务器处理原始音频,然后将其传递给仍使用“快速混音器”线程的Android实现的媒体服务器。输入和输出服务器都使用OpenSL Sched_FIFO策略中的调度。
图由Eric Serre提供
此修改的结果是一个非常令人满意的45毫秒RTL。此实现是Intel Atom SoC和用于此工作的平板电脑设计的一部分。此测试在Intel软件开发平台上进行,可通过Intel合作伙伴软件开发计划获取。
OpenSL和SCHED_FIFO策略的实现展示了在上述指定硬件平台上高效处理往返实时音频,但并非所有设备都可用。使用本文档中示例的任何应用程序测试都必须在特定设备上进行,并且可以提供给合作伙伴软件开发人员。
摘要
本文讨论了如何使用OpenSL在应用程序中创建回调和缓冲区队列,以遵循Android音频开发方法。它还展示了Intel为使用修改后的媒体框架提供的一种低延迟音频信号选择所做的努力。要进行此实验并测试低延迟路径,开发人员必须遵循Android音频开发的OpenSL设计,并在Android Kit Kat 4.4.2或更高版本上使用Intel软件开发平台。
贡献者
Eric Serre,Intel Corporation
Victor Lazzarini