Android端使用AAduio实现高性能音频

AAudio 是在 Android O 版本中引入的全新 Android C API,与AAudio类似的是oboe库。此 API 专为需要低延迟的高性能音频应用而设计。应用通过读取数据并将数据写入流来与 AAudio 进行通信。AAudio 在应用与 Android 设备的音频输入端及输出端之间传输音频数据。应用通过读取和写入以 AAudioStream 结构表示的音频流来传入和传出数据。这些读写操作可以是阻塞式调用或非阻塞式调用。

Android平台有AudioTrack、AAudio和OpenSL三种方式播放音频,如下图所示:

一、AAudio介绍

1、音频设备

音频设备是硬件接口或虚拟端点,用作连续的数字音频数据流的来源或接收器。每个流都连接到单个音频设备。

我们可调用AudioManager方法 getDevices() 来发现可用的音频设备。Android 设备上的每个音频设备都具有唯一ID,我们可使用该ID将音频流与特定音频设备绑定。但在大多数情况下,我们可以让 AAudio 选择默认设备。连接到流的音频设备负责确定该流是用于输入还是输出,因为流只能在一个方向传输数据。定义流时,我们可以指定其方向。打开流时,Android 会检查确保音频设备与流方向一致。

2、共享模式

AAudio的stream流具有两种共享模式:

  • AAUDIO_SHARING_MODE_EXCLUSIVE 表示该流对其音频设备进行独占访问;该设备不可供任何其他音频流使用。如果音频设备已在使用当中,流可能无法对其进行独占访问。独占流的延迟时间往往较短,但连接断开的可能性也较大。如果不再需要独占流,应尽快关闭,以便其他应用访问该设备。独占流可以最大限度缩短延迟时间。
  • AAUDIO_SHARING_MODE_SHARED 允许AAudio混合音频。AAudio会将分配给同一设备的所有共享流混合。

我们可以在创建流时指定共享模式。默认情况下,共享模式为SHARED

3、音频格式

通过流传递的数据具有如下的数字音频属性:

  • 音频格式
  • 采样率
  • 声道数

AAudio支持两种音频格式:AAUDIO_FORMAT_PCM_I16和AAUDIO_FORMAT_PCM_FLOAT

4、创建音频流

AAudio 库遵循构建者设计模式,并提供 AAudioStreamBuilder。

4.1 创建 AAudioStreamBuilder:

AAudioStreamBuilder *builder;
aaudio_result_t result = AAudio_createStreamBuilder(&builder);

4.2 构建参数,其中参数选项如下:

AAudioStreamBuilder_setDeviceId(builder, deviceId);
AAudioStreamBuilder_setDirection(builder, direction);
AAudioStreamBuilder_setSharingMode(builder, mode);
AAudioStreamBuilder_setSampleRate(builder, sampleRate);
AAudioStreamBuilder_setChannelCount(builder, channelCount);
AAudioStreamBuilder_setFormat(builder, format);
AAudioStreamBuilder_setBufferCapacityInFrames(builder, frames);

4.3 用AAudioStreamBuilder创建流:

AAudioStream *stream;
result = AAudioStreamBuilder_openStream(builder, &stream);

4.4 删除AAudioStreamBuilder:

AAudioStreamBuilder_delete(builder);

5、使用AAudio音频流

5.1 状态转换

AAudio有5种状态:

  • 打开
  • 已开始
  • 已暂停
  • 已刷新
  • 已停止

仅当流处于“已开始”状态时,数据才会通过流来传输。如需转换流所处的状态,请使用以下其中一个函数请求状态转换:

aaudio_result_t result;
result = AAudioStream_requestStart(stream);
result = AAudioStream_requestStop(stream);
result = AAudioStream_requestPause(stream);
result = AAudioStream_requestFlush(stream);

AAudio流的状态图如下:

我们可以在调用开始、停止或刷新请求后,将相应的过渡状态用作inputState。不要在调用 AAudioStream_close() 之后调用 waitForStateChange(),因为流在关闭时会立即被删除。此外,不要在另一线程运行waitForStateChange() 时调用AAudioStream_close()。 

5.2 音频流读写

在流启动后,可通过两种方法来处理流中的数据:

对于传输指定帧数的阻塞读取或写入操作,请将 timeoutNanos 设置为大于零。 对于非阻塞调用,请将 timeoutNanos 设置为零。在这种情况下,结果将是传输的实际帧数。读取输入值时,我们应该验证是否已读取正确数量的帧。如果未读取正确数量的帧,缓冲区可能包含未知的数据,从而引起音频干扰。我们可以在缓冲区中填入零,以产生静音效果:

aaudio_result_t result =
    AAudioStream_read(stream, audioData, numFrames, timeout);
if (result < 0) {
  // Error!
}
if (result != numFrames) {
  // pad the buffer with zeros
  memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0,
      sizeof(sample_type) * (numFrames - result) * samplesPerFrame);
}

5.3 关闭音频流

AAudio的stream使用完毕,需要关闭:

AAudioStream_close(stream);

5.4 断开连接的音频流

有三种情况会导致音频流断开连接:

  • 关联的音频设备不再处于连接状态(例如,拔出耳机时);
  • 内部发生错误;
  • 音频设备发生变化;

流断开连接时,其状态变为“已断开连接”,所有尝试执行 AAudioStream_write 或其他函数的操作都会返回错误,必须停止并关闭已断开连接的流。

如果是使用数据回调(而不是某个直接读写方法),那么当流断开连接时,不会收到任何返回代码。如需在发生这种情况时接收通知,请编写 AAudioStream_errorCallback 函数,然后使用 AAudioStreamBuilder_setErrorCallback() 注册该函数。在错误回调线程中收到连接已断开的通知,则流的停止和关闭必须从其他线程中完成。否则可能出现死锁。如下所示:

void errorCallback(AAudioStream *stream,
                   void *userData,
                   aaudio_result_t error) {
    // Launch a new thread to handle the disconnect.
    std::thread myThread(my_error_thread_proc, stream, userData);
    myThread.detach(); // Don't wait for the thread to finish.
}

6、性能优化

我们可以通过调整内部缓冲区、使用高优先级线程回调、设置性能模式,来优化音频应用性能。

6.1 调整缓冲区

AAudio的每个音频设备维护内部缓冲区,通过调整缓冲区来以最大限度减少延迟时间。缓冲区容量是缓冲区中可以存放的数据总量。调用 AAudioStreamBuilder_setBufferCapacityInFrames() 来设置容量。应用不必使用缓冲区的全部容量,可以设置 AAudio 填充缓冲区空间的大小上限。通过控制缓冲区大小,确定填充缓冲区所需的脉冲串数,从而控制延迟时间。我们可以使用 AAudioStreamBuilder_setBufferSizeInFrames() 方法来处理缓冲区大小。

应用播放音频时,会将数据阻塞式写入缓冲区。AAudio以离散的脉冲串从缓冲区中读取数据。每个脉冲串都包含多个音频帧,而且通常小于所读取的缓冲区大小。脉冲串的大小及速率由系统控制,而这些属性由音频设备的电路指定。虽然无法更改脉冲串的大小或速率,但可以根据内部缓冲区所含的脉冲串数量来设置内部缓冲区大小。通常,当 AAudioStream 的缓冲区大小是所报告脉冲串大小的倍数时,延迟时间最短。工作示意图如下:

优化缓冲区空间大小的一种方法是从较大的缓冲区开始,逐渐将其减小直至开始出现缓冲区不足现象,再稍稍将其调大。我们也可以从较小的缓冲区空间大小开始,如果出现缓冲区不足现象,则增大缓冲区空间大小,直至输出再次流畅为止。缓冲区优化循环的示例如下:

int32_t previousUnderrunCount = 0;
int32_t framesPerBurst = AAudioStream_getFramesPerBurst(stream);
int32_t bufferSize = AAudioStream_getBufferSizeInFrames(stream);
int32_t bufferCapacity = AAudioStream_getBufferCapacityInFrames(stream);

while (running) {
    result = writeSomeData();
    if (result < 0) break;

    if (bufferSize < bufferCapacity) {
        int32_t underrunCount = AAudioStream_getXRunCount(stream);
        if (underrunCount > previousUnderrunCount) {
            previousUnderrunCount = underrunCount;
            // Try increasing the buffer size by one burst
            bufferSize += framesPerBurst;
            bufferSize = AAudioStream_setBufferSize(stream, bufferSize);
        }
    }
}

6.2 使用高优先级线程回调

应用程序从原始线程中读取或写入音频数据,可能会被抢占或遇到定时抖动,进而可能引起音频卡顿。使用较大的缓冲区有助于避免此类干扰,但是如果缓冲区较大,音频延迟时间也会更长。对于要求延迟时间较短的应用,音频流可以使用一个异步回调函数,将数据传输到您的应用并从中传输数据。AAudio 会在优先级较高的线程中执行该回调,这有助于改善性能。该回调函数的原型如下所示:

typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames);

使用AUAduioStream流构建方式来注册回调: 

AAudioStreamBuilder_setDataCallback(builder, myCallback, myUserData);

使用 AAudio 可以处理多个流。我们可以将其中一个流作为主流,并在用户数据中传递指向其他流的指针。针对主流注册回调,对其他流使用非阻塞 I/O。该回调从输入流执行非阻塞读取,以将数据放入输出流的缓冲区:

aaudio_data_callback_result_t myCallback(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames) {
    AAudioStream *inputStream = (AAudioStream *) userData;
    int64_t timeout = 0;
    aaudio_result_t result =
        AAudioStream_read(inputStream, audioData, numFrames, timeout);

  if (result == numFrames)
      return AAUDIO_CALLABCK_RESULT_CONTINUE;
  if (result >= 0) {
      memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0,
          sizeof(sample_type) * (numFrames - result) * samplesPerFrame);
      return AAUDIO_CALLBACK_RESULT_CONTINUE;
  }
  return AAUDIO_CALLBACK_RESULT_STOP;
}

7、设置性能模式

每个AAudioStream都具有性能模式,共有三种模式:

  • AAUDIO_PERFORMANCE_MODE_NONE 是默认模式。这种模式使用在延迟时间与节能之间取得平衡的基本流;
  • AAUDIO_PERFORMANCE_MODE_LOW_LATENCY 使用较小的缓冲区和经优化的数据路径,以减少延迟时间;
  • AAUDIO_PERFORMANCE_MODE_POWER_SAVING 使用较大的内部缓冲区,以及以延迟时间为代价换取节能优势的数据路径;

我们可以通过调用 setPerformanceMode() 来设置性能模式。如果应用程序的缩短延迟时间比节能更重要,请使用 AAUDIO_PERFORMANCE_MODE_LOW_LATENCY。如果应用程序中节能比缩短延迟时间更重要,请使用 AAUDIO_PERFORMANCE_MODE_POWER_SAVING。这在播放音乐的应用(例如流式音频或 MIDI 文件播放器)中很常见。

为了尽量减少延迟时间,您必须将 AAUDIO_PERFORMANCE_MODE_LOW_LATENCY 性能模式与高优先级回调配合使用。示例如下:

// Create a stream builder
AAudioStreamBuilder *streamBuilder;
AAudio_createStreamBuilder(&streamBuilder);
AAudioStreamBuilder_setDataCallback(streamBuilder, dataCallback, nullptr);
AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);

// Use it to create the stream
AAudioStream *stream;
AAudioStreamBuilder_openStream(streamBuilder, &stream);

二、音频采样

从 Android 5.0 (Lollipop) 起,音频重采样器完全基于衍生自 Kaiser 加窗 sinc 函数的 FIR 滤波器。Kaiser 加窗 sinc 函数具有以下属性:

  • 可以轻松地计算其设计参数(阻带波纹、过渡带宽、截止频率和滤波器长度)。
  • 相对于整体能量来说,此函数几乎是减弱阻带能量的最佳选择。

设计参数将根据内部质量确定结果和所需的采样比自动计算。根据设计参数,将生成加窗 sinc 滤波器。对于音乐用途,44.1 kHz 至 48 kHz重采样器的生成质量要比任意频率转换的质量高。

但是,重采样器可能会带来少量的通带波纹和混叠谐波噪声,并且它们会导致过渡带中出现一些高频丢失,因此请避免不必要地使用重采样器。

1、重采样最佳做法

一般而言,最好选择适合设备的采样率,通常为 44.1 kHz 或 48 kHz。使用大于 48 kHz 的采样率一般会导致质量下降,因为必须使用重采样器回放文件。

1.1 使用简单的重采样比

重采样器可以在下列几种模式下运行:

  • 固定多相模式。每个多相的滤波器系数都预先计算。
  • 插值多相模式。每个多相的滤波器系数必须从最接近的两个预计算多相插入。

重采样器在固定多相模式下最快,此时输入速率与输出速率之比 L/M(除去最大公约数)中的 M 小于 256。例如,对于 44100 至 48000 转换,L = 147,M = 160。

在固定多相模式下,采样率被锁定,不会发生变化。在插值多相模式下,采样率为近似值。在 48kHz 设备上播放时,采样率偏移一般为几小时内一个样本。

1.2 使用上采样(而不是下采样)来更改采样率

可以动态更改采样率。此类更改的粒度基于内部缓冲(通常为数百个样本),而不是逐个样本更改。这可以用于音效。

下采样时,请不要动态更改采样率。如果在创建音轨后更改采样率,降采样时与原始采样率存在 5%-10% 的差异可能会触发滤波器重新计算(以正确抑制混叠)。这会消耗计算资源,并且如果滤波器被实时替换,还可能听到咔嚓声。

1.3 将下采样限制为不大于 6:1

下采样通常由硬件设备要求触发。如果在下采样时使用采样率转换器,为了取得良好的混叠抑制效果,请尝试将下采样比限制为不大于 6:1(例如,不大于 48000:8000 的下采样)。滤波器长度将调整以匹配下采样比,但是如果下采样比较高,将牺牲更多的过渡带宽来避免过度增加滤波器长度。上采样则没有类似的混叠担忧。请注意,某些音频管道可能会阻止大于2:1的下采样。

2、使用浮点音频

使用浮点数表示音频数据可以显著增强高性能音频应用中的音频质量。浮点数具有以下优势:

  • 更宽的动态范围。
  • 动态范围内一致的准确性。
  • 更多余量,可以避免在中间计算和瞬态期间发生截断情况。

尽管可以增强音质,浮点数也存在特定的劣势:

  • 浮点数占用更多内存。
  • 浮点数运算具有意外特性,例如加法不遵守结合律。
  • 由于四舍五入或者数字不稳定算法,浮点运算有时会牺牲一些算数精度。
  • 若要有效使用浮点数,就需要对它有更充分的理解,才能获得准确且可重现的结果。

之前,浮点数曾因不可用或者速度慢而被诟病。这种情况仍然存在于低端和嵌入式处理器中。但是,对于现代移动设备上的处理器来说,硬件浮点数的性能已经与整数相似(某些情况下甚至比后者更快)。现代 CPU 还支持 SIMD(单指令多数据),这种技术可以进一步提升性能。

2.1 有关浮点音频的最佳做法

下面的最佳做法可以帮助避免浮点运算存在的问题:

  • 对频率较低的运算(例如计算滤波器系数)使用双精度浮点数;
  • 注意运算顺序;
  • 为中间值声明显式变量;
  • 大量地使用括号;
  • 如果获得 NaN 或无穷结果,使用二分搜索;

对于浮点音频,音频格式编码 AudioFormat.ENCODING_PCM_FLOAT 的使用方式类似于使用 ENCODING_PCM_16_BIT 或 ENCODING_PCM_8_BIT 指定 AudioTrack 数据格式。相应的过载方法 AudioTrack.write() 将采用浮点数组来提供数据。

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
实现输入音频文件输出音频内容,你需要用到AndroidAudioRecord和AudioTrack类。这里提供一个简单的实现过程: 1. 首先,你需要在AndroidManifest.xml文件中添加录音权限: ```xml <uses-permission android:name="android.permission.RECORD_AUDIO" /> ``` 2. 在你的Activity中创建AudioRecord实例,设置音频参数,开始录音: ```java int audioSource = MediaRecorder.AudioSource.MIC; int sampleRate = 44100; int channelConfig = AudioFormat.CHANNEL_IN_MONO; int audioFormat = AudioFormat.ENCODING_PCM_16BIT; int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); AudioRecord audioRecord = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, bufferSize); byte[] buffer = new byte[bufferSize]; audioRecord.startRecording(); ``` 3. 接下来,你需要创建一个输出文件,将录制的音频数据写入到该文件中: ```java File outputFile = new File(Environment.getExternalStorageDirectory(), "output.pcm"); FileOutputStream outputStream = new FileOutputStream(outputFile); while (true) { int read = audioRecord.read(buffer, 0, bufferSize); outputStream.write(buffer, 0, read); } ``` 4. 最后,你可以使用AudioTrack类将输出文件中的音频数据播放出来: ```java int streamType = AudioManager.STREAM_MUSIC; int mode = AudioTrack.MODE_STREAM; int sampleRate = 44100; int channelConfig = AudioFormat.CHANNEL_OUT_MONO; int audioFormat = AudioFormat.ENCODING_PCM_16BIT; int bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); AudioTrack audioTrack = new AudioTrack(streamType, sampleRate, channelConfig, audioFormat, bufferSize, mode); audioTrack.play(); File inputFile = new File(Environment.getExternalStorageDirectory(), "output.pcm"); FileInputStream inputStream = new FileInputStream(inputFile); byte[] buffer = new byte[bufferSize]; while (inputStream.read(buffer) != -1) { audioTrack.write(buffer, 0, buffer.length); } ``` 注意:这只是一个简单的实现过程,实际应用中你需要处理更多的异常情况和错误处理,以及对音频数据的解码和编码等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

徐福记456

您的鼓励和肯定是我创作动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值