音视频基础能力之 Android 音频篇 (五):史上最全的音频渲染总结

871 阅读9分钟

涉及硬件的音视频能力,比如采集、渲染、硬件编码、硬件解码,通常是与客户端操作系统强相关的,就算是跨平台的多媒体框架也必须使用平台原生语言的模块来支持这些功能。

本系列文章将详细讲述移动端音视频的采集、渲染、硬件编码、硬件解码这些涉及硬件的能力该如何实现。本文为该系列文章的第 5 篇,将详细讲述在 Android 平台下如何实现音频的渲染。

前言

在之前的文章,我们详细介绍了 Android 平台下音频采集的几种方式,像 Java API 的 AudioRecord、MediaRecord,c/c++ 接口的 OpenSL、AAudio、Oboe。有关于音频渲染的接口和音频采集的接口是一一对应的,如下图所示。

image.png

由于之前都介绍过相关接口的使用,本文将着重讲解下相关 API 差异性的地方,或者之前没有提及到的地方。如果您是第一次观看我们的文章,非常建议您浏览下之前的相关文章,这对您掌握本文的知识点大有裨益。

音视频基础能力之 Android 音频篇(一): 音频采集

音视频基础能力之 Andoid 音频篇(二):音频录制

音视频基础能力之 Android 音频篇 (三):高性能音频采集

需注意 MediaPlayer 的有关内容本文将不会介绍,它更偏向于媒体文件的播放,不仅包含音频、视频,还涉及到解封装和解码。而本文讲述的重点是音频裸流 PCM 的播放,准备在完成视频篇内容之后再来详细介绍,敬请期待。

Demo 的代码链接在文章的底部,如果您对实现细节不想关注的话,烦请移步。

AudioTrack

使用 Java API AudioTrack 的好处是集成方便,不需要接触 c/c++ 相关的东西,底层系统都帮你完成了,只要你传一些参数即可,但是不适用于低延迟的音频场景。

初始化

首先,需要关注下播放音频帧大小是否大于系统支持的最小音频缓冲 buffer 的大小,若大于则需要调整,这点之前在之前的文章已详细讲解。

val bytesPerFrame = channels * BITS_PER_SAMPLE / 8
byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * (sampleRate / BUFFERS_PER_SECOND))
Log.i(this.tag, "byteBuffer.capacity: ${byteBuffer?.capacity()}")
val channelConfig = channelCountToConfiguration(channels)
var minBufferSizeInBytes = AudioTrack.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT)
Log.i(this.tag, "AudioTrack.getMinBufferSize:$minBufferSizeInBytes")

if (minBufferSizeInBytes < byteBuffer!!.capacity()) {
    Log.i(this.tag, "AudioTrack.getMinBufferSize returns an invalid value.")
    return ErrorCode.INIT_ERROR.ordinal
}

接下来就是 AudioTrack 对象的构造了,在 Android API 21 上下,实现方式有所不同。来看下代码:

// API小于21
AudioTrack audioTrack = new AudioTrack(stream_type, sampleRateInHz, channelConfig,
        AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes, AudioTrack.MODE_STREAM);
        
// API大于等于21
AudioTrack audioTrack = AudioTrack(AudioAttributes.Builder().setUsage(usageAttribute).setContentType(contentType).build(),
    AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_PCM_16BIT).setSampleRate(sampleRate).setChannelMask(channelConfig).build(),
    minBufferSizeInBytes, AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE)

API 21 以上 AudioTrack 的构造明显变复杂了,参数不仅变多了且更细化了。

  • AudioAttributes 音频属性,高版本 Android 替代 streamType 这个概念,强调的是音频的用途和内容。音频路由篇曾介绍过,系统会根据音频属性来决定音频路由的抉择和音量调整等。
  • AudioFormat 音频规格,采样率、通道数、位宽等老演员了😁
  • byteSizeInBytes 设定单次渲染的字节数
  • mode 渲染模式,MODE_STATIC 适用于渲染的整体音频量不大的,需要反复播放的业务场景,例如音效;MODE_STREAM 是需要持续不断的让系统去渲染的音频场景,这里先了解下,后续会结合源码详细介绍下。
  • sessionId 可以通过该值来达到精准的音频会话管理,比如音量控制、路由控制、音效控制等。如果没有此需求,填 AudioManager.AUDIO_SESSION_ID_GENERATE 即可。

开始渲染

在开启渲染之前,得单独准备一条音频的渲染线程,音频 PCM 数据需要通过这条线程写入系统底层音频合流服务。

线程执行流中的关键步骤如下:

  1. 设置音频渲染线程高优先级,高优先级的线程会被系统 CPU 优先调度,且获取的 CPU 时长比普通线程长一些。设置高优先级的意义也是为了播放音频流更加的流畅,因为人耳对音频的延迟、卡顿的敏感度是非常高的。
// step1: 设置音频渲染线程的优先级,THREAD_PRIORITY_URGENT_AUDIO 算是非常高的线程优先级了,
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
  1. 准备 PCM 数据源,我们这里为了演示就直接从文件里面读取数据了。
val sizeInBytes = byteBuffer!!.capacity()
val byteArray = ByteArray(sizeInBytes)

while(keepAlive) {
	//....  
	val readLen = inputStream.read(byteArray)
  if (readLen < sizeInBytes) {
    Log.w(this.tag, "read pcm buffer not equals sizeInBytes.")
    if (readLen < 0) { //-1 means read end.
        keepAlive = false
        return
    }
	}

	//放到 ByteBuffer 中去
  byteBuffer?.put(byteArray)
  byteBuffer?.rewind()
	//....
}
  1. 向 AudioTrack 对象写入音频数据,最后一个参数表示此方式是同步执行还是异步执行。如果是异步执行,会将数据抛向队列中去,立马返回;如果是同步执行,会等待写入成功为止。如果是低延迟场景,建议还是选择同步返回,因为我们可以根据写入的耗时来计算是否需要丢包等逻辑。如果选择异步返回,一般是需要保证数据渲染的完整性。
val len = audioTrack!!.write(byteBuffer!!, sizeInBytes, AudioTrack.WRITE_BLOCKING)
if (len != sizeInBytes) {
    Log.e(this.tag, "AudioTrack.write played invalid bytes.")
}
if (len < 0) {
    keepAlive = false
    return
}
byteBuffer?.rewind()

开启渲染逻辑如下:

// step1: 调整 AudioTrack 进入 PLAYING 状态:
audioTrack!!.play()

// step2: 开启音频渲染线程轮转
audioThread = AudioTrackThread()
audioThread!!.start()

停止渲染

//step1: 退出音频渲染线程
audioThread?.stopThread()
audioThread = null

//step2: 释放AudioTrack资源
audioTrack?.flush()
audioTrack?.release()
audioTrack = null

OpenSL

如果您对实现 OpenSL 的代码流程或者细节还不熟悉的话,可以先阅读下 音视频基础能力之 Android 音频篇 (三):高性能音频采集

之前的文章说过 OpenSL 由于功能比较全面加上接口拓展性比较强,所以导致接口非常的多。实现起来的代码量非常的多,这里就不贴代码了,我们结合时序图来讲述下重点环节。

audio_playout-%E7%AC%AC_2_%E9%A1%B5.jpg

初始化流程

  • sl_engine_ 对象将必要的音频参数通过 CreateAudioPlayer 函数构造出来 sl_player_object_ 对象。sl_player_object_ 是完成音频渲染的关键对象。
  • 通过 sl_player_object_ 分别构造出 play_config 对象,sl_player 对象、simple_buffer_queue_ 对象。
    • SLAndroidConfigurationItf (Android 平台的拓展) 提供了设置 Android 平台特性相关的参数,类似于 streamType 等。
    • SLPlayItf 提供播放状态的设置/获取与相关回调,详细的可参考接口类。
    • SLAndroidSimpleBufferQueueItf 主要提供音频 buffer Queue 相关的控制、回调接口。

核心播放相关代码

  1. 监听 BufferQueue 的数据回调请求
 result = (*simple_buffer_queue_)->RegisterCallback(simple_buffer_queue_, SimpleBufferQueueCallback, this);
 if (result != SL_RESULT_SUCCESS) {
   AV_LOGE("SimpleBufferQueue RegisterCallback failed, reason:%s", GetSLErrorString(result));
   return SV_PLAY_INIT_ERROR;
 }
  1. 开启渲染,激活 BufferQueue
// step1: 向 BufferQueue 填充数据,以激活。另外在开启之前填充的好处,就是刚开始渲染音频的时候
// 不会有杂音。
if (!FillBufferQueue(false)) {  
  return SV_FILL_BUFFER_ERROR;
}

// step2: 设置 sl_player_ 状态为 PLAYING 状态,之前设置的回调函数 SimpleBufferQueueCallback 
// 开始工作
auto result = (*sl_player_)->SetPlayState(sl_player_, SL_PLAYSTATE_PLAYING);
if (result != SL_RESULT_SUCCESS) {
  AV_LOGW("Set playing state failed.");  // Maybe permission problem.
  return SV_START_PLAY_ERROR;
}
  1. 向 BufferQueue 填充音频数据
//之前向 simple_buffer_queue_ 注册的回调函数
void SVOpenslRender::SimpleBufferQueueCallback(SLAndroidSimpleBufferQueueItf caller, void* context) {

	// step1: 从文件中读取音频pcm数据,举个例子而已
	const size_t per_size = sizeof(SLint16);
  const size_t buf_size = sample_rate_ / 100 * channels_;
  auto len = fread(audio_buffers_.get(), per_size, buf_size, file_);
  
  // step2: 向 BufferQueue 喂数据
  auto * binary_data = reinterpret_cast<SLint8 *>(audio_buffers_.get());
  size_t size =  sample_rate_ / 100 * channels_ * 2;
  auto result = (*simple_buffer_queue_)->Enqueue(simple_buffer_queue_, binary_data, size);
  if (result != SL_RESULT_SUCCESS) {
    AV_LOGE("Enqueue failed: %s", GetSLErrorString(result));
    return false;
  }
}

如何在项目中引入 OpenSL

// 头文件
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h> //Android相关拓展

// 库文件
target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        log
        OpenSLES)

AAudio

如何在项目中引入 AAudio ?

// step1: 引入头文件
#include <aaudio/AAudio.h>

// step2: 引入库文件
target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        log
        aaudio)

渲染的关键流程

  1. AAudio 参数配置,和采集不同的是需要设置 Direction 为 AAUDIO_DIRECTION_OUTPUT。
AAudioStreamBuilder_setDeviceId(builder_, AAUDIO_UNSPECIFIED);
AAudioStreamBuilder_setSampleRate(builder_, sample_rate);
AAudioStreamBuilder_setChannelCount(builder_, channels);
AAudioStreamBuilder_setFormat(builder_, AAUDIO_FORMAT_PCM_I16);
AAudioStreamBuilder_setSharingMode(builder_, AAUDIO_SHARING_MODE_SHARED);
AAudioStreamBuilder_setDirection(builder_, AAUDIO_DIRECTION_OUTPUT);
AAudioStreamBuilder_setPerformanceMode(builder_, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setDataCallback(builder_, DataCallback, this);
AAudioStreamBuilder_setErrorCallback(builder_, ErrorCallback, this);
  1. open stream
auto result = AAudioStreamBuilder_openStream(builder_, &stream_);
if (result != AAUDIO_OK) {
  AV_LOGE("AAudio open stream failed, reason:%s", AAudio_convertResultToText(result));
  return SV_PLAY_INIT_ERROR;
}
  1. 开启渲染 & 填充数据
// step1: request start.
auto result = AAudioStream_requestStart(stream_);
if (result != AAUDIO_OK) {
  AV_LOGE("AAudio request start error, reason:%s", AAudio_convertResultToText(result));
  return SV_START_PLAY_ERROR;
}

// step2: fill audio audio.
aaudio_data_callback_result_t
SVAAudioRender::DataCallback(AAudioStream* stream, void* user_data, void* audio_data, int32_t num_frames) {
	// read pcm data from file.
  if (!render->ReadPlayoutData(num_frames)) {
	  AV_LOGW("Read playout data failed.");
	  return AAUDIO_CALLBACK_RESULT_STOP;
	}
	// copy audio data.
	memcpy(audio_data, render->audio_buffers_.get(), num_bytes);
}

播放控制 (optional)

如果您的项目对音频播放延迟非常重视的话,需要了解下以下知识点。下图由 Android 官方提供,介绍了 AAudio 是如何从缓冲区消费数据的。

image 1.png

AAudio 会以离散的脉冲串从缓冲区读取数据,具体脉冲串的大小及速率是由系统控制的,而属性是由音频设备的电路决定的。虽然我们没法直接控制脉冲串的大小和速率,但是可以不断优化调整设置的缓存区大小(图中 Size 的范围)。官方给出的优化建议是:

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

示例代码:

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

int32_t bufferCapacity = AAudioStream_getBufferCapacityInFrames(stream);

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

    // Are we getting underruns? -- 数据的生产跟不上消费,需要增加缓冲区
    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);
        }
    }
}

Oboe

Oboe 是 Google 的一个开源项目,存在的意义和使用场景之前的文章也曾提及过,这里就不赘述了。由于它在使用上和 AAudio 如出一辙,读者可以根据笔者提供的源码来进一步学习。下面介绍下如何在项目中引入 Oboe 库。

  • build.gradle 配置(模块级别)
android {
		//...
	  buildFeatures {
        prefab true
    }
}

dependencies {
    implementation 'com.google.oboe:oboe:1.9.0'
}
  • cmake 配置
find_package (oboe REQUIRED CONFIG)

target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        log
        oboe::oboe)
  • 头文件引入
#include <oboe/Oboe.h>

最后

以上就是本文的所有内容了,主要介绍了 Android 平台下音频渲染的几种方式。本文为音视频基础能力 - Android 音频篇的第 5 篇,后续精彩内容,敬请期待。

github samle code: github.com/Sound-Visio…

如果您觉得以上内容对您有所帮助的话,欢迎关注我们运营的公众号 声知视界,会定期推送音视频技术、移动端技术为主轴的科普类、基础知识类、行业资讯类等文章。