- 示例工程
- 移动开发
【免费下载链接】ndk-samples
Android NDK samples with Android Studio
Native Audio 是 ndk-samples 仓库中一个以 C++ OpenSL ES 音频 API 为核心、通过 JNI 与 Java 层交互的完整音频示例,覆盖了播放(Buffer Queue / URI / Asset FD 三种数据源)与录音两大能力。阅读本文后,你将掌握 OpenSL ES 引擎与输出混音器的搭建流程、三种音频播放器与录音器的完整创建范式、采样率重采样与线程互斥的实战写法,并了解为何 Android 11 起官方推荐迁移到 Oboe 库。
示例定位与技术选型
根据 native-audio/README.md 的说明,该示例通过 JNI 在 C++ 侧调用 OpenSL ES API 完成声音的播放与录制,其创建的录音器与播放器并不走 fast audio path(快速音频路径)。这意味着它是一个侧重"功能完整性"而非"最低延迟"的教学示例,适合用来理解 OpenSL ES 的完整对象模型。
示例同时展示了 Android Studio 的 CMake 插件 + C++ 支持的工作流,源码位于 native-audio/app/src/main/cpp/native-audio-jni.c,构建配置见 native-audio/app/src/main/cpp/CMakeLists.txt。
重要的弃用警告
README 特别强调:
OpenSL ES 自 Android 11 起已被弃用(deprecated),官方推荐开发者改用 Oboe 库。
这是阅读本示例时必须牢记的前提:OpenSL ES 的 API 形态与对象模型仍有很强的学习价值,但新项目应优先采用 hello-oboe 示例所展示的 Oboe 方案。
前置条件与运行步骤
README 给出的运行环境要求为Android Studio 2.2+ 并随附 NDK(仓库当前配置对构建系统版本要求更高,实际以工程内 gradle/libs.versions.toml 与各模块 Gradle 配置为准)。完整启动流程:
- 下载并启动 Android Studio;
- 打开示例目录(即仓库中的
native-audio目录); - 打开File/Project Structure...,点击Download或Select NDK location配置 NDK 路径;
- 点击Tools/Android/Sync Project with Gradle Files同步 Gradle;
- 点击Run/Run 'app'部署到设备。
运行后界面包含 Hello / Android / Sawtooth 三个短音效按钮、内嵌 soundtrack(背景音乐)、混音器控制(Reverb、Mute)、URI 播放控制组(播放/暂停/循环/声道/音量/声像)、录音与回放按钮,布局定义见 native-audio/app/src/main/res/layout/main.xml。
应用整体架构:引擎、输出混音器与三类播放器
从 native-audio-jni.c 的全局对象可以看出 OpenSL ES 典型的对象层次:
- Engine(引擎):
engineObject/engineEngine,一切对象的创建入口; - Output Mix(输出混音器):
outputMixObject,所有播放器的音频汇聚目标,并挂载可选的环境混响(Environmental Reverb)接口; - 三类播放器:
- Buffer Queue Player(
bqPlayerObject):播放内存中的 PCM 短音效; - URI Player(
uriPlayerObject):播放网络/本地 URI; - Asset FD Player(
fdPlayerObject):通过文件描述符播放 APK 内嵌资源;
- Buffer Queue Player(
- Recorder(录音器):
recorderObject,通过SLAndroidSimpleBufferQueue接收 PCM 数据。
引擎与输出混音器创建流程
Java_com_example_nativeaudio_NativeAudio_createEngine展示了标准三步法:
// 创建引擎 result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL); // Realize 后才可用 result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE); // 获取 ENGINE 接口,用于创建其他对象 result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);随后通过CreateOutputMix创建输出混音器,并将环境混响声明为非必需接口(SL_BOOLEAN_FALSE):
const SLInterfaceID ids[1] = {SL_IID_ENVIRONMENTALREVERB}; const SLboolean req[1] = {SL_BOOLEAN_FALSE}; result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, ids, req);取混响接口后,用SL_I3DL2_ENVIRONMENT_PRESET_STONECORRIDOR预设(石质走廊)设置属性;源码注释明确指出该接口可能因特性缺失、CPU 负载过高或未申请/授予MODIFY_AUDIO_SETTINGS权限而失败,因此示例对失败结果选择忽略——这正是 AndroidManifest.xml 声明MODIFY_AUDIO_SETTINGS权限的原因。
权限声明一览
AndroidManifest.xml 声明了三个权限,与功能一一对应:
| 权限 | 用途 | 对应功能 |
|---|---|---|
RECORD_AUDIO | 创建音频录音器 | 录音按钮 |
MODIFY_AUDIO_SETTINGS | 使用环境混响等音频效果 | Reverb 按钮 |
INTERNET | URI 播放器访问网络资源 | URI soundtrack |
Java 侧(NativeAudio.java)对RECORD_AUDIO使用了运行时权限申请(ActivityCompat.requestPermissions+onRequestPermissionsResult回调),拒绝时会弹出 Toast 提示,并允许用户重试。
Buffer Queue 播放器:内存 PCM 播放与重采样
Buffer Queue 播放器用于播放三份内置 PCM 数据:hello、android两份预录音频(由 hello_clip.h 与 android_clip.h 以 C 字符串数组形式内嵌,均为8 kHz 单声道 16 位有符号小端PCM),以及运行时合成的锯齿波sawtoothBuffer(8000 帧,由__attribute__((constructor))在加载时生成)。
数据源配置
createBufferQueueAudioPlayer中音频源配置如下:
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2}; // 最多 2 个 buffer SLDataFormat_PCM format_pcm = { SL_DATAFORMAT_PCM, 1, // 单声道 SL_SAMPLINGRATE_8, SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, SL_SPEAKER_FRONT_CENTER, SL_BYTEORDER_LITTLEENDIAN};Java 侧在onCreate中通过AudioManager.getProperty(PROPERTY_OUTPUT_SAMPLE_RATE)与PROPERTY_OUTPUT_FRAMES_PER_BUFFER查询设备原生采样率与每缓冲帧数(仅 API 17+),传入原生层。当sampleRate > 0时:
if (bqPlayerSampleRate) { format_pcm.samplesPerSec = bqPlayerSampleRate; // 采样率单位:毫赫兹(milliHz) }源码注释指出"一旦采样率设置为原生采样率,将触发 fast audio path";同时为兼容 fast path,创建播放器时动态裁剪接口数量——fast path 不支持SL_IID_EFFECTSEND,因此CreateAudioPlayer的接口个数在bqPlayerSampleRate ? 2 : 3间切换。
简易重采样实现
由于内置剪辑为 8 kHz、录音为 16 kHz,而播放器可能以设备原生采样率运行,createResampledBuf实现了一个仅支持整数倍上采样的朴素重采样器:
- 要求
bqPlayerSampleRate % srcRate == 0(即目标采样率必须能被源采样率整除),否则返回 NULL 回退到原始 8 kHz 播放; - 采用"样本复制"(sample-and-hold)方式,将每个源样本重复
upSampleRate次; - 播放录音回放(CLIP_PLAYBACK)时若无法重采样,则退化为简单降采样:每两个 16 kHz 样本取一个,将 16 kHz 数据折半为 8 kHz。
for (int sample = 0; sample < srcSampleCount; sample++) { for (int dup = 0; dup < upSampleRate; dup++) { *workBuf++ = src[sample]; } }播放回调与多缓冲
bqPlayerCallback是缓冲队列播放完成回调:每次一个 buffer 播完,若nextCount > 0且数据有效,则Enqueue下一块数据继续播放;否则释放重采样缓冲并解锁互斥锁。注释明确说明:流式播放时应至少预入队 2 个 buffer,而本示例只入队一个"巨型 buffer"以简化逻辑。
URI 播放器与 Asset FD 播放器
URI 播放器
createUriAudioPlayer使用SLDataLocator_URI定位数据源,格式声明为SL_CONTAINERTYPE_UNSPECIFIED的 MIME 流。源码注释指出:无效 URI 在 Android 上并非在创建时被检测,而是在 prepare/prefetch 阶段报错;因此创建后需检查Realize返回值,失败时调用Destroy并返回JNI_FALSE。
该播放器还演示了完整的控制接口获取:SL_IID_PLAY(播放/暂停)、SL_IID_SEEK(循环)、SL_IID_MUTESOLO(声道静音/独奏)、SL_IID_VOLUME(音量)。Java 界面上的静音/独奏/音量/声像控制均通过一组getMuteSolo()/getVolume()辅助函数选择"当前可用的播放器"来路由,其中音量以millibel(千分之一贝尔)为单位——SeekBar 从 100 递减时,衰减量millibel = (100 - progress) * -50。
已知问题:URI 流式播放损坏
README 的 Known Issues 明确记录:URI Player 流式播放存在缺陷(broken),对应 upstream issue #229。Java 代码中同样可见其影响——onCreate末尾对 SDK > 19 的设备禁用了全部 URI 相关控件(uri_soundtrack、pause_uri、loop_uri、声道、音量、声像等),并注释内部 bug idb/29321867,等待后续系统版本修复后重新开放。因此该部分代码在当前 Android 版本上主要用于演示 API 形态。
Asset FD 播放器
createAssetAudioPlayer演示了如何播放 APK 内嵌资源:
// 通过 JNI 获取 AAssetManager AAssetManager* mgr = AAssetManager_fromJava(env, assetManager); AAsset* asset = AAssetManager_open(mgr, utf8, AASSET_MODE_UNKNOWN); // 将 asset 打开为文件描述符 off_t start, length; int fd = AAsset_openFileDescriptor(asset, &start, &length); AAsset_close(asset); // 以 SL_DATALOCATOR_ANDROIDFD 作为数据源创建播放器 SLDataLocator_AndroidFD loc_fd = {SL_DATALOCATOR_ANDROIDFD, fd, start, length};这是"嵌入式 soundtrack"按钮的实现路径:Java 侧传入AssetManager与文件名(background.mp3,其版权信息见 native-audio/app/src/main/assets/README.txt),原生层打开文件描述符后创建播放器并默认开启整文件循环(SetLoop(SL_BOOLEAN_TRUE, 0, SL_TIME_UNKNOWN))。
录音器:单缓冲采集与互斥保护
createAudioRecorder的音频源为SL_IODEVICE_AUDIOINPUT输入设备,数据接收端为SLAndroidSimpleBufferQueue,PCM 格式固定为16 kHz 单声道 16 位小端。录音缓冲为recorderBuffer[16000 * 5](约 5 秒)。
SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, NULL};startRecording的核心动作是:先停止上一次录音并Clear缓冲队列 → 入队一个空 buffer →SetRecordState(SL_RECORDSTATE_RECORDING)启动采集。bqRecorderCallback在缓冲填满时停止录音(SL_RECORDSTATE_STOPPED),并将recorderSize置为有效长度;播放回放(CLIP_PLAYBACK)则把这段 16 kHz 数据重采样或降采样到 8 kHz 后送入 Buffer Queue 播放器,完成"录-放"闭环。
并发保护:音频引擎互斥锁
示例用pthread_mutex_t实现录音与播放的互斥(audioEngineLock),源码注释说明了设计动机:防止"录音尚未结束又发起第二次录音/播放"导致崩溃。所有入口(selectClip、startRecording)均使用pthread_mutex_trylock非阻塞尝试加锁,失败即返回JNI_FALSE让上层稍后重试;锁的释放分散在播放/录音回调与入队失败分支中,shutdown时统一pthread_mutex_destroy。
这种"trylock + 回调解锁"的模式虽然在回调路径中手工管理锁较为繁琐,但清晰演示了音频回调线程与 UI 线程之间的同步边界,是理解原生音频引擎线程模型的良好切入点。
JNI 绑定与 CMake 构建
Java 侧通过System.loadLibrary("native-audio-jni")加载动态库(见 NativeAudio.java 的静态初始化块),并以public static native声明了createEngine、createBufferQueueAudioPlayer、createUriAudioPlayer、createAssetAudioPlayer、selectClip、enableReverb、createAudioRecorder、startRecording、shutdown等 18 个原生方法;C 侧以Java_com_example_nativeaudio_NativeAudio_前缀逐一实现。
构建配置 native-audio/app/src/main/cpp/CMakeLists.txt 非常精简:
cmake_minimum_required(VERSION 3.22.1) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall") add_library(native-audio-jni SHARED native-audio-jni.c) target_link_libraries(native-audio-jni android log OpenSLES)三个链接库各司其职:android(AAssetManager 等原生接口)、log(日志输出)、OpenSLES(OpenSL ES 音频 API)。
小结与迁移建议
本示例完整覆盖了 OpenSL ES 的核心编程范式:引擎 → 输出混音器 →(播放器/录音器)的对象层次、Buffer Queue / URI / AndroidFD 三种数据源、回调驱动的缓冲管理、以及 JNI 与 CMake 的集成方式。需要再次强调 README 中的结论:OpenSL ES 已从 Android 11 起弃用,新项目应使用 hello-oboe 中演示的 Oboe 库;若需更低延迟的音频路径,可参考本仓库 audio-echo 示例对 Oboe 的实际应用。对于学习目的,本示例依然是理解 Android 原生音频对象模型的经典教材。
- 示例工程
- 移动开发
【免费下载链接】ndk-samples
Android NDK samples with Android Studio
相关推荐
ndk-samples 之 Audio-Echo:基于 OpenSL ES 低延迟音频路径的回声示例全解析
ndk samples 之 Audio Echo:基于 OpenSL ES 低延迟音频路径的回声示例全解析 本篇技术指南以 Android NDK 官方示例仓库
示例工程移动开发Android NDK音频回声示例解析:OpenSL ES低延迟实现
Android NDK音频回声示例解析:OpenSL ES低延迟实现 项目概述 这个Android NDK音频回声示例展示了如何利用OpenSL ES API在
示例工程移动开发Android Native音频完全指南:OpenSL ES、Native Audio与Oboe库3个样本对比(ndk-samples)
Android Native音频完全指南:OpenSL ES、Native Audio与Oboe库3个样本对比(ndk samples) 在 Android 官
示例工程移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考