1 环境
这次主要是看看audio在安卓平台的实现,用是瑞芯微RK3568开发板。
本来是想还是用之前的WM8960模块,但是很无语发现它板子上的I2S接口并没有引出来。引出的口只有I2C,SPI,还有UART。
和之前用的WM8960比较一下:
参数 | RK809-5 | WM8960 |
|---|---|---|
芯片定位 | PMIC + Audio Codec | 独立 Audio Codec |
ADC | 24-bit 立体声 | 24-bit 立体声 |
DAC | 24-bit 立体声 | 24-bit 立体声 |
最高采样率 | 192kHz | 48kHz |
I2S | 支持 | 支持 |
PLL | 内置 | 内置 |
麦克风输入 | 单端/差分 | 多路输入,支持伪差分 |
MICBIAS | 支持麦克风输入,具体偏置配置看电路 | 集成低噪声 MICBIAS |
ALC | 支持 | 支持 |
Noise Gate | 支持 | 支持 |
耳机功放 | Class-AB,Real Ground | Class-AB 耳机驱动 |
扬声器功放 | Class-D,最高约1.3W | 双声道 Class-D,每声道1W(8Ω、5V) |
控制接口 | I2C | I2C |
封装 | QFN68,7×7mm | QFN,5×5mm |
看来功能基本上都差不多,RK809甚至采样率还更好。不过WM8960 是专门设计的音频 Codec,在模拟输入、音频路由、增益控制和音频应用灵活性方面可能更好点。
算了,将就平台本来的audio去看吧,不折腾了。
2 整体架构
关于安卓的整体音频架构,之前写过一篇:Android音频系统_android uac-CSDN博客,不过之前没有实际用版本验证。下面这个图是流传很广的安卓音频架构的图。
不过用AI跑了一下,这个图估计已经是上古时代的了。整体应该还是有不少的变化。其实新的架构如下:
App (Java/Native)->AudioFlinger (系统服务,audioserver)->Binder IPC->
android.hardware.audio.service(供应商 HAL 进程)->ALSA 驱动 (/dev/snd/*)->硬件音频 Codec / DAC / 喇叭
改动是:
| 原图 | 这块板的实际情况 |
|---|---|
| AudioFlinger、AudioPolicyService 放在Media Server中 | 已拆分到audioserver;板上mediaserver仍存在,但不再包办这些音频服务 |
| HAL 画成简单的一层调用 | 板上有独立的android.hardware.audio.service进程,对外提供HIDL 7.0接口 |
| 内核写成 ALSA/OSS/Custom Driver | 本板模拟音频走ALSA+ASoC,包含 I2S 控制器驱动、RK809 Codec 驱动及声卡连接配置 |
| 框中的源码路径 | 有些已经变化,例如 AudioTrack、AudioRecord 等客户端代码主要在frameworks/av/media/libaudioclient |
新的核心就是两个,AudioServer和android.hardware.audio.service。一个是上层服务,一个是HAL。
3 底层
3.1 驱动
这次还是用板子实际来看吧。先用AI把这个板子上的音频链路跑了一遍。目前在电路上实际接了一个喇叭,一个麦克风,一个耳机插座。
RK3568 与 RK809之间的接口:
| 信号 | 作用 |
|---|---|
| MCLK | 音频主时钟 |
| SCLK/BCLK | 音频位时钟 |
| LRCK | 左右声道/帧同步时钟 |
| RK3568 SDO0 → RK809 SDI | 播放数据 |
| RK809 SDO → RK3568 SDI0 | 录音数据 |
此外,RK3568 通过 I²C0、地址 0x20 控制 RK809,设置供电、音频通路、增益和静音等。I²C 负责配置,I2S 负责传声音数据。
目前在系统中已经有了三个底层声卡:
| 声卡编号 | 声卡 | 实际用途 |
|---|---|---|
| 0 | rockchip,rk809-codec | 板载麦克风、耳机、喇叭;支持录音和播放 |
| 1 | rockchip,bt | I2S3 接蓝牙模块的 SCO/PCM 语音链路 |
| 2 | rockchip,hdmi | HDMI 音频输出 |
3.2 tinyalsa
Android这边底层和一般Linux基本没太大区别。不过ALSA的界面是阉割了,给了一个青春版tinyalsa,很多功能(混音、重采样、设备选择和音频路由等功能)收到自己的上层去处理了。
| 功能 | alsa-lib 可以提供的机制 | Android 通常由谁负责 |
|---|---|---|
| 多个应用同时播放、混音 | dmix等插件 | AudioFlinger |
| 格式、采样率转换 | plug、`rate` 等插件 | 音频框架及相应处理模块 |
| 选择输出设备 | 设备名、配置及插件机制 | AudioPolicy+HAL |
| 读写 PCM、操作 Mixer | ALSA 接口 | TinyALSA |
TinyALSA常见命令:
| 工具 | 全称 / 作用 | 对标 amixer | 核心用途 | 参数要点 |
|---|---|---|---|---|
tinymix | tiny mixer 控制 | amixer | 查看 / 设置 mixer 控件(路由、增益、开关) | -D声卡号;-a列出枚举全部值;get看当前值;直接带名字和值 = 设置 |
tinyplay | tiny player | 播放 wav | 放音测试,验证播放通路 | -D声卡;-pperiod;-nbuffer;只支持 wav |
tinycap | tiny capture | arecord | 录音采集,验证录音通路 | -D;-r采样率;-c声道;-b位深;-pperiod;-T时长 (秒) |
tinypcminfo | tiny pcm info | — | 查看声卡 PCM 支持能力(格式 / 采样率) | -D;-p查 playback;-c查 capture |
常用命令组合:
| 场景 | 命令序列 | 备注 |
|---|---|---|
| ✅ 板载麦录音(推荐模板) | tinymix "Playback Path" OFF tinymix "Capture MIC Path" "MAIN MIC" tinycap /sdcard/main_mic.wav -T 8 | 关闭播放通路避免回授啸叫;录制 8 秒 |
| ✅ 带麦耳机录音(4 段耳机) | tinymix "Playback Path" HP tinymix "Capture MIC Path" "HP MIC" tinycap /sdcard/hp_mic.wav -T 8 | 耳机听 + 耳机麦收音 |
| ✅ 喇叭外放播放 | tinymix "Playback Path" SPK tinyplay /sdcard/test.wav | 喇叭出声 |
| ✅ 无麦耳机播放(3 节耳机) | tinymix "Playback Path" HP_NO_MIC tinyplay /sdcard/test.wav | 只有听,没有麦克风 |
| ✅ 带麦耳机播放(4 段耳机) | tinymix "Playback Path" HP tinyplay /sdcard/test.wav | 耳机播放,后续可以切 HP MIC 录音 |
| ✅ 关闭全部音频通路 | tinymix "Playback Path" OFF tinymix "Capture MIC Path" "MIC OFF" | 调试收尾,切断通路 |
| ✅ 自定义参数录音模板 | tinymix "Capture MIC Path" "MAIN MIC" tinycap /sdcard/rec.wav -D 0 -r 48000 -c 2 -b 16 -p 1024 -T 10 | 48k、立体声、16bit、period=1024 帧,录 10s |
这些命令可以直接操作底层硬件,但是需要root权限,一般的app不能直接访问。需要上层接口。
4 android.hardware.audio.service
android.hardware.audio.service 是 Android 系统中负责音频硬件抽象层(Audio HAL)的核心服务进程。它是 Android 高层软件栈(如 AudioFlinger、Java 音频 API)与底层 Linux 内核音频驱动(ALSA / SoC 声音驱动)之间的“桥梁和翻译官”。
作用是:
音频设备的控制与路由: 管理扬声器(Speaker)、听筒(Earpiece)、麦克风、蓝牙耳机、USB 声卡等硬件的开关、音量调节、音频路径切换(Routing)。
数据流管理(Stream Management): 负责创建和维护音频输入流(
IStreamIn,录音)和输出流(IStreamOut,播放)。通过共享内存(Shared Memory / FMQ 快速消息队列)高效接收来自AudioFlinger的 PCM 音频数据包。对接底层 ALSA 驱动: HAL 进程内部最终会调用 Linux 标准的
alsa-lib(如snd_pcm_writei),将 PCM 数据写入内核的声卡驱动缓冲区,完成物理发声。
| 工作 | HAL 实际要处理的内容 |
|---|---|
| 选设备 | Android 要从耳机输出,具体应该打开哪张声卡、哪个 PCM 设备? |
| 配置音频流 | 使用什么采样率、位深、声道数?硬件是否支持? |
| 配置缓冲区 | 每次传多少帧、准备几块缓冲区,兼顾延迟和稳定性 |
| 打开硬件通路 | 设置 Mixer,打开 PCM;处理打开失败的情况 |
| 持续传输数据 | 将上层送来的 PCM 写入声卡;录音时反向读取 |
| 必要的数据转换 | 例如你这份 RK 实现中的部分重采样处理 |
| 处理状态变化 | 暂停后进入待机、恢复时重新打开;切换输出时关闭或重建相应通路 |
| 向上报告状态 | 缓冲区大小、延迟、播放位置等,让 Android 安排后续数据并维持音画同步 |
4.1 Android定义
代码位置在/data/rk3568/android12_sdk/hardware/interfaces/audio/common/all-versions/default/service/
hardware/interfaces/应该所有的HAL接口定义都在这边。
服务的定义service.cpp如下:
/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #define LOG_TAG "audiohalservice" #include <string> #include <vector> #include <binder/ProcessState.h> #include <cutils/properties.h> #include <hidl/HidlTransportSupport.h> #include <hidl/LegacySupport.h> #include <hwbinder/ProcessState.h> using namespace android::hardware; using android::OK; using InterfacesList = std::vector<std::string>; /** Try to register the provided factories in the provided order. * If any registers successfully, do not register any other and return true. * If all fail, return false. */ template <class Iter> static bool registerPassthroughServiceImplementations(Iter first, Iter last) { for (; first != last; ++first) { if (registerPassthroughServiceImplementation(*first) == OK) { return true; } } return false; } int main(int /* argc */, char* /* argv */ []) { ::android::ProcessState::initWithDriver("/dev/vndbinder"); // start a threadpool for vndbinder interactions ::android::ProcessState::self()->startThreadPool(); const int32_t defaultValue = -1; int32_t value = property_get_int32("persist.vendor.audio.service.hwbinder.size_kbyte", defaultValue); if (value != defaultValue) { ALOGD("Configuring hwbinder with mmap size %d KBytes", value); ProcessState::initWithMmapSize(static_cast<size_t>(value) * 1024); } configureRpcThreadpool(16, true /*callerWillJoin*/); // Automatic formatting tries to compact the lines, making them less readable // clang-format off const std::vector<InterfacesList> mandatoryInterfaces = { { "Audio Core API", "android.hardware.audio@7.0::IDevicesFactory", "android.hardware.audio@6.0::IDevicesFactory", "android.hardware.audio@5.0::IDevicesFactory", "android.hardware.audio@4.0::IDevicesFactory", }, { "Audio Effect API", "android.hardware.audio.effect@7.0::IEffectsFactory", "android.hardware.audio.effect@6.0::IEffectsFactory", "android.hardware.audio.effect@5.0::IEffectsFactory", "android.hardware.audio.effect@4.0::IEffectsFactory", } }; const std::vector<InterfacesList> optionalInterfaces = { { "Soundtrigger API", "android.hardware.soundtrigger@2.3::ISoundTriggerHw", "android.hardware.soundtrigger@2.2::ISoundTriggerHw", "android.hardware.soundtrigger@2.1::ISoundTriggerHw", "android.hardware.soundtrigger@2.0::ISoundTriggerHw", }, { "Bluetooth Audio API", "android.hardware.bluetooth.audio@2.1::IBluetoothAudioProvidersFactory", "android.hardware.bluetooth.audio@2.0::IBluetoothAudioProvidersFactory", }, // remove the old HIDL when Bluetooth Audio Hal V2 has offloading supported { "Bluetooth Audio Offload API", "android.hardware.bluetooth.a2dp@1.0::IBluetoothAudioOffload" } }; // clang-format on for (const auto& listIter : mandatoryInterfaces) { auto iter = listIter.begin(); const std::string& interfaceFamilyName = *iter++; LOG_ALWAYS_FATAL_IF(!registerPassthroughServiceImplementations(iter, listIter.end()), "Could not register %s", interfaceFamilyName.c_str()); } for (const auto& listIter : optionalInterfaces) { auto iter = listIter.begin(); const std::string& interfaceFamilyName = *iter++; ALOGW_IF(!registerPassthroughServiceImplementations(iter, listIter.end()), "Could not register %s", interfaceFamilyName.c_str()); } joinRpcThreadpool(); }服务启动配置android.hardware.audio.service.rc:
service vendor.audio-hal /vendor/bin/hw/android.hardware.audio.service class hal user audioserver # media gid needed for /dev/fm (radio) and for /data/misc/media (tee) group audio camera drmrpc inet media mediadrm net_bt net_bt_admin net_bw_acct wakelock context_hub capabilities BLOCK_SUSPEND ioprio rt 4 task_profiles ProcessCapacityHigh HighPerformance onrestart restart audioserver当前这块板向 Android 音频框架提供的是 HIDL 7.0,主要接口如下:
| 接口 | 主要职责 | 代表方法 |
|---|---|---|
IDevicesFactory | 服务入口,打开音频设备 | openDevice()、`openPrimaryDevice()` |
IDevice | 设备级控制、创建录放音流及连接通路 | openOutputStream()、`openInputStream()`、`setMicMute()`、`createAudioPatch()`、`setConnectedState()` |
IPrimaryDevice | 继承IDevice,增加主音频设备、通话及蓝牙相关控制 | setMode()、`setVoiceVolume()`、`setBtScoWidebandEnabled()` |
IStream | 输入、输出流共用的配置和生命周期接口 | getAudioProperties()、`setAudioProperties()`、`standby()`、`close()` |
IStreamOut | 播放流、输出延迟、播放进度及暂停等控制 | prepareForWriting()、`getLatency()`、`getPresentationPosition()`、`pause()`、`drain()` |
IStreamIn | 录音流、采集进度及麦克风信息 | prepareForReading()、`getCapturePosition()`、`getActiveMicrophones()` |
IStreamOutCallback | 播放流异步通知 | onWriteReady()、`onDrainReady()`、`onError()` |
IStreamOutEventCallback | 输出流事件通知 | onCodecFormatChanged() |
IEffectsFactory | 枚举、查询和创建音效实例 | getAllDescriptors()、`getDescriptor()`、`createEffect()` |
IEffect | 配置和运行具体音效 | setConfig()、`enable()`、`disable()`、`setParameter()`、`prepareForProcessing()` |
4.2 RK适配
所有的适配代码在hardware/rockchip/
音频代码在:
主要的工作是:
/data/rk3568/android12_sdk/hardware/rockchip/audio/tinyalsa_hal/
├── audio_hw.c 声卡识别、HDMI/蓝牙/采集特殊处理
├── alsa_route.c 应用通路配置
└── codec_config/ 不同 Codec 的控件配置表
5 AudioServer
audioserver是 Android 音频的集中管理进程,主要负责音频流、混音、设备路由和硬件访问。它里面最核心的是 AudioFlinger 和 AudioPolicyService。
整体功能上和才写过的Linux的PipeWire差不多。
5.1 功能和实现
功能如下:
| 功能 | 主要负责者 | 具体作用 |
|---|---|---|
| 创建播放、录音通道 | AudioFlinger | 为 App 的 AudioTrack/AudioRecord 创建服务端音轨和录音通道 |
| 多路混音 | AudioFlinger | 将音乐、通知、导航等多个音轨混合输出 |
| 音量、静音及必要的格式转换 | AudioFlinger | 应用各音轨音量,按输出要求进行必要的重采样、声道等处理 |
| 音效处理 | AudioFlinger 及音效实现 | 组织音效链,例如均衡器、录音预处理;实际支持取决于设备 |
| 选择设备和通路 | AudioPolicyService/AudioPolicyManager | 决定使用耳机、喇叭、蓝牙或 HDMI,以及使用哪个输入输出流 |
| 协调并发使用 | 音频策略+AudioFlinger | 根据策略处理多个应用、录放音及特殊音频模式的资源使用 |
| 管理流的运行状态 | AudioFlinger | 启动、待机、关闭和资源回收,管理播放/采集线程与缓冲区 |
| 访问 Audio HAL | 经libaudiohal | 打开硬件音频流、读写数据、设置参数、查询延迟和播放位置 |
理论上其实app可以直接访问HAL。但 谷歌把它集中到audioserver,是为了让多个应用安全、有序地共享音频硬件。正常第三方 App 也没有直接访问音频 HAL 的权限。
| 场景 | 如果各 App 直接操作 HAL | audioserver提供的协调 |
|---|---|---|
| 音乐和导航同时发声 | 争用硬件流,或各自重复实现混音 | AudioFlinger 统一混音 |
| 不同格式、采样率的声音 | 每个 App 自己匹配硬件要求 | 统一处理必要的转换 |
| 耳机插入或拔出 | 多个 App 各自切换,容易冲突 | 根据音频策略协调路由 |
| 一个 App 卡住或退出 | 可能留下占用的资源 | 统一管理和回收音频流 |
| 通话、录音、媒体同时工作 | 各 App 难以协调全局状态 | 配合系统策略管理并发和权限 |
代码在/frameworks/av/media/audioserver/
具体实现在其它地方:
# AudioFlinger:音频流、混音、录放音线程
frameworks/av/services/audioflinger/AudioFlinger.cpp# 音频策略服务:对外提供策略接口
frameworks/av/services/audiopolicy/service/AudioPolicyService.cpp# 默认策略实现:设备选择、路由等决策
frameworks/av/services/audiopolicy/managerdefault/AudioPolicyManager.cpp
一般情况下,audioserver使用AOSP原生的即可,不用怎么修改。
5.2 App调用
使用AudioTrack这套库,就可以直接调用audioserver。用的是JNI/Binder。
例子:
import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioTrack; private void playTone() { new Thread(() -> { int sampleRate = 48000; short[] pcm = new short[sampleRate]; // 单声道,1 秒 for (int i = 0; i < pcm.length; i++) { pcm[i] = (short) ( Math.sin(2 * Math.PI * 440 * i / sampleRate) * 2000 ); } AudioTrack track = new AudioTrack.Builder() .setAudioAttributes(new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build()) .setAudioFormat(new AudioFormat.Builder() .setSampleRate(sampleRate) .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) .build()) .setTransferMode(AudioTrack.MODE_STATIC) .setBufferSizeInBytes(pcm.length * 2) .build(); try { track.write(pcm, 0, pcm.length); track.play(); while (track.getPlaybackHeadPosition() < pcm.length) { Thread.sleep(10); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { track.release(); } }).start(); }