1. 从"能听到"到"拿得到":Android音频流截取到底难在哪
做过Android音频相关开发的人大概都有过这样的经历:想做一个音乐可视化效果,或者想给正在播放的音频加一层实时处理,又或者想做一个"听歌识曲"式的本地分析工具,结果第一步就卡住了——怎么把系统正在播放的音频流拿到手?
这个问题的核心难点在于,Android的音频架构从设计之初就把"播放"和"采集"分成了两条相对独立的通路。播放走的是输出通路,采集走的是输入通路,两者在应用层默认是不互通的。你调用AudioRecord去录音,拿到的是麦克风采集到的环境声音,而不是系统内部正在播放的那路音频。这就导致很多人第一次尝试时,录出来的是一片安静,或者只有扬声器漏出来的微弱声音。
那有没有办法拿到系统内部正在播放的音频流呢?答案是有的,但不同Android版本给出的方案差异很大,而且各自有各自的限制。这篇文章就把我这些年在这个方向上踩过的坑、试过的方案、以及最终跑通的思路,完整地梳理一遍。不管你是想做音频可视化、做实时音频分析,还是单纯想搞清楚Android音频子系统的运作方式,应该都能从中找到能直接用的东西。
需要先说明一点:本文讨论的是在合法合规前提下,对设备自身播放音频进行技术研究与开发的场景,比如做音乐播放器的可视化功能、做音频分析工具、做无障碍辅助功能等。所有操作都应在用户知情且授权的范围内进行,不要拿去做任何侵犯版权或隐私的事情。
先给一个全局的结论,方便你建立预期:
| 方案 | 适用版本 | 能否拿到系统混音 | 主要限制 |
|---|---|---|---|
| AudioRecord + 特定音源 | 较老版本 | 部分可以 | 新版本已废弃对应音源 |
| MediaProjection 音频捕获 | Android 10+ | 可以 | 需用户授权,部分机型有兼容问题 |
| 应用内音频回调 | 全版本 | 仅限自家应用 | 拿不到其他应用的音频 |
| 系统级音频框架Hook | 需系统权限 | 可以 | 需要Root或系统签名 |
这张表基本概括了所有可行路径。下面我会逐条展开,把每条路的原理、实操步骤、以及实际跑起来会遇到的问题讲清楚。
2. 先搞清楚Android音频子系统的数据流向
在动手写代码之前,有必要先把Android音频的数据流向理一遍。很多人上来就抄代码,结果遇到问题完全不知道从哪排查,就是因为对底层通路没有概念。
2.1 从应用层到硬件的完整链路
一个典型的Android音频播放链路大致是这样的:应用层通过MediaPlayer或AudioTrack把音频数据交给AudioFlinger,AudioFlinger是Android音频系统的核心服务,负责混音和路由,它把多个应用的声音混合之后,再通过Audio HAL(硬件抽象层)送到音频编解码器,最终从扬声器或耳机输出。
关键点在于,AudioFlinger这个混音环节是所有应用音频的汇聚点。理论上,只要能在AudioFlinger的输出环节拿到数据,就能截取到系统正在播放的所有音频。但普通应用是没有权限直接访问AudioFlinger内部缓冲区的,这就是为什么需要借助一些特殊的API或机制。
2.2 采集通路和播放通路为什么是分开的
Android把采集和播放分开设计,主要是出于隐私和安全的考虑。如果任意应用都能随意录制系统内部播放的音频,那就会带来严重的隐私问题——比如偷偷录下用户的通话、录下其他应用播放的私密内容。所以Google在架构上做了隔离,普通应用只能通过麦克风采集环境声音。
理解了这一点,你就能明白为什么后面要讲的MediaProjection方案需要用户显式授权——因为它本质上是在打破这层隔离,所以必须让用户知情。
2.3 几个容易混淆的概念
在继续之前,先把几个容易搞混的术语区分一下:
- AudioRecord:用于采集音频的类,通常从麦克风读取数据。
- AudioTrack:用于播放音频的类,把PCM数据送到输出设备。
- AudioFlinger:系统级音频服务,负责混音和路由。
- AudioPlaybackCapture:Android 10引入的API,允许应用捕获其他应用的音频播放。
- MediaProjection:屏幕捕获服务,Android 10起也支持音频捕获。
这几个概念里,AudioPlaybackCapture和MediaProjection是我们在新版本上截取系统音频的主要抓手,后面会重点讲。
3. Android 10之前的困境与老版本可行思路
Android 10是一个分水岭。在它之前,官方并没有提供正规的API来捕获系统内部音频,开发者只能想各种"偏方"。
3.1 那些年被废弃的音频源
早期Android的MediaRecorder.AudioSource里有一些特殊的音源常量,比如REMOTE_SUBMIX,它的设计初衷是用于远程音频混合,理论上可以拿到系统混音的一部分。但这个音源需要CAPTURE_AUDIO_OUTPUT权限,而这个权限是系统级权限,普通应用根本申请不到。
还有一个AudioSource.MIC配合扬声器外放的方式,就是让手机外放声音,然后用麦克风录回来。这个方案听起来很土,但在某些场景下确实能用。它的缺点是音质损失严重,而且会录进环境噪音,延迟也大。我早期做过一个音乐可视化的Demo,就是用这种方式,效果只能说勉强能看,频谱数据抖动很厉害。
3.2 系统权限路线的现实门槛
如果你有系统签名或者Root权限,那路子就宽多了。可以直接在AudioFlinger层面做拦截,或者修改音频策略配置,把输出流复制一份出来。但这种方案对绝大多数开发者来说不现实,因为:
- 需要系统级签名,普通应用拿不到。
- 需要修改系统镜像,用户不会为了你的应用去刷机。
- 不同厂商的ROM差异巨大,适配成本极高。
所以这条路只适合做系统预装应用或者定制ROM的团队,普通应用开发者基本可以放弃。
3.3 应用内捕获的局限性
还有一个思路是只捕获自己应用的音频。如果你的目标只是给自己的播放器加可视化,那完全不需要去截取系统音频,直接在播放环节做文章就行。比如用AudioTrack播放时,在写入数据之前把PCM数据复制一份出来做分析。这种方式最简单、最稳定,而且没有任何权限问题。
但它的局限也很明显:只能拿到自己应用播放的音频,拿不到其他应用的。如果你的需求是"截取系统正在播放的音乐",那这个方案就不够用了。
4. Android 10+的官方方案:AudioPlaybackCapture实战
从Android 10(API 29)开始,Google终于提供了官方的音频捕获API,这就是AudioPlaybackCapture。它允许应用捕获其他应用正在播放的音频,但有几个重要的前提条件。
4.1 核心前提:目标应用必须允许被捕获
这是最容易被忽略的一点。AudioPlaybackCapture并不是无条件捕获所有音频的,被捕获的应用必须在其AndroidManifest.xml中声明android:allowAudioPlaybackCapture="true",或者其AudioAttributes的usage属性设置为USAGE_MEDIA或USAGE_GAME,且capturePolicy允许捕获。
换句话说,如果某个应用明确声明了不允许被捕获(比如一些注重隐私的应用),那你是拿不到它的音频的。这是系统层面的保护机制,绕不过去。
4.2 配置MediaProjection并申请权限
要使用AudioPlaybackCapture,首先需要拿到MediaProjection。虽然名字叫"媒体投影",听起来像是屏幕录制,但Android 10起它也承担了音频捕获的授权职责。
申请流程大致如下:
// 第一步:请求MediaProjection权限 MediaProjectionManager projectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); Intent intent = projectionManager.createScreenCaptureIntent(); startActivityForResult(intent, REQUEST_CODE_MEDIA_PROJECTION);用户会看到一个授权弹窗,同意之后,在onActivityResult里拿到resultCode和data,然后用它们创建MediaProjection:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE_MEDIA_PROJECTION && resultCode == RESULT_OK) { MediaProjection projection = projectionManager.getMediaProjection(resultCode, data); // 接下来用projection创建AudioRecord } }这里有个坑要注意:MediaProjection在Android 14上对每个应用的使用次数做了限制,不能无限次重复申请。所以拿到之后要妥善管理,用完及时释放。
4.3 构建AudioRecord并指定播放捕获配置
拿到MediaProjection之后,就可以构建一个专门用于捕获播放音频的AudioRecord了。关键配置如下:
AudioFormat captureFormat = new AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setSampleRate(44100) .setChannelMask(AudioFormat.CHANNEL_IN_STEREO) .build(); AudioPlaybackCaptureConfiguration config = new AudioPlaybackCaptureConfiguration.Builder(projection) .addMatchingUsage(AudioAttributes.USAGE_MEDIA) .addMatchingUsage(AudioAttributes.USAGE_GAME) .build(); AudioRecord audioRecord = new AudioRecord.Builder() .setAudioFormat(captureFormat) .setBufferSizeInBytes(bufferSize) .setAudioPlaybackCaptureConfig(config) .build();这里的addMatchingUsage决定了你要捕获哪些用途的音频。USAGE_MEDIA对应音乐、视频等媒体播放,USAGE_GAME对应游戏音效。你可以按需添加。
bufferSize的计算建议用AudioRecord.getMinBufferSize()拿到最小值,然后乘以一个系数(比如2到4倍),避免缓冲区太小导致丢帧。
4.4 读取数据与常见异常处理
配置好之后,调用audioRecord.startRecording(),然后在一个独立线程里循环read数据:
short[] buffer = new short[bufferSize / 2]; while (isCapturing) { int read = audioRecord.read(buffer, 0, buffer.length); if (read > 0) { // 处理PCM数据,比如做FFT分析或写入文件 } }实测下来,这个方案在大部分主流机型上都能正常工作,但有几个常见问题:
- 部分机型返回静音数据:某些厂商的ROM对
AudioPlaybackCapture支持不完整,可能读出来全是0。这种情况只能做兼容处理,检测到静音时给出提示。 - 采样率不匹配:如果目标音频的采样率和你的
AudioRecord配置不一致,系统会做重采样,可能引入轻微失真。 - 延迟问题:捕获到的音频相比实际播放会有一定延迟,通常在几十毫秒到上百毫秒,做实时可视化时要注意补偿。
提示:
AudioPlaybackCapture捕获的是混音后的音频,也就是说如果同时有多个应用在播放,你会拿到混合后的结果,无法单独分离某一个应用的声音。
5. 应用内音频流的直接截取:更简单也更可控
如果你的需求只是给自己的应用做音频分析,那完全不需要走MediaProjection那套复杂流程。直接在音频数据产生的源头做拦截,是最简单也最可靠的方式。
5.1 在AudioTrack写入前复制PCM数据
用AudioTrack播放音频时,数据是通过write()方法写入的。你只需要在写入之前,把同一份数据复制到另一个缓冲区,就可以拿来做分析了:
byte[] audioData = getNextAudioChunk(); // 正常播放 audioTrack.write(audioData, 0, audioData.length); // 复制一份用于分析 analysisBuffer.offer(audioData.clone());这种方式没有任何权限问题,延迟也最低,因为数据还没经过系统混音和重采样。做频谱分析、节拍检测这类应用,用这个方案是最合适的。
5.2 MediaPlayer场景下的Visualizer用法
如果你用的是MediaPlayer而不是AudioTrack,那可以用Visualizer这个类来获取音频波形和FFT数据。它的用法也不复杂:
Visualizer visualizer = new Visualizer(mediaPlayer.getAudioSessionId()); visualizer.setCaptureSize(Visualizer.getCaptureSizeRange()[1]); visualizer.setDataCaptureListener(new Visualizer.OnDataCaptureListener() { @Override public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) { // 拿到波形数据 } @Override public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) { // 拿到FFT数据 } }, Visualizer.getMaxCaptureRate() / 2, true, true); visualizer.setEnabled(true);Visualizer的好处是直接给你算好了FFT,省去了自己写傅里叶变换的麻烦。但它只能用于自己应用的音频会话,拿不到其他应用的。
5.3 两种方式的选型对比
到底选哪种,取决于你的具体需求:
| 维度 | AudioTrack复制 | Visualizer | AudioPlaybackCapture |
|---|---|---|---|
| 能否捕获其他应用 | 否 | 否 | 是 |
| 权限要求 | 无 | 无 | 需用户授权 |
| 数据粒度 | 原始PCM | 波形/FFT | 原始PCM |
| 延迟 | 最低 | 低 | 中等 |
| 兼容性 | 全版本 | 全版本 | Android 10+ |
我的建议是:能用自己的音频就用自己,实在需要系统级的再上AudioPlaybackCapture。不要为了炫技去用复杂方案,稳定可靠才是第一位的。
6. 踩坑实录:那些让我熬夜排查的兼容性问题
这一节专门讲讲我在实际项目中遇到的各种问题,都是文档里不会写的,但实际开发中大概率会碰到。
6.1 授权弹窗被系统拦截
在部分定制ROM上,createScreenCaptureIntent()弹出的授权窗口会被系统安全机制拦截,用户点了同意但onActivityResult返回的是RESULT_CANCELED。排查了很久才发现,是某些ROM把屏幕捕获权限和音频捕获权限做了额外管控。
解决办法是在申请之前先检查MediaProjectionManager是否可用,并且在用户拒绝后给出明确的引导提示,而不是直接崩溃或静默失败。
6.2 采样率协商失败导致的杂音
有一次在某个机型上,捕获到的音频全是刺耳的噪音。查了半天发现是采样率不匹配:目标音频是48000Hz,而我配置的是44100Hz,系统重采样时出了问题。后来改成动态获取目标音频的采样率,或者统一用48000Hz,问题就消失了。
这个坑的教训是:不要硬编码采样率,尽量用系统推荐值或者动态适配。
6.3 后台运行时的权限回收
Android 10之后,后台应用对MediaProjection的使用受到严格限制。如果你的应用退到后台,捕获可能会被系统暂停甚至回收。要解决这个问题,通常需要配合前台服务(ForegroundService),并在通知栏给出明确的"正在捕获音频"提示。
这里要注意,前台服务的类型在Android 14上有了新要求,需要声明foregroundServiceType为mediaProjection,否则会抛异常。
6.4 缓冲区大小设置不当引发的卡顿
AudioRecord的缓冲区大小如果设置得太小,会导致read()频繁阻塞,进而引发卡顿甚至丢帧。如果设置得太大,又会增加延迟。我的经验是:
- 先用
getMinBufferSize()拿到最小值。 - 然后乘以2到4倍作为实际缓冲区大小。
- 如果做实时性要求高的处理,可以适当减小倍数,但要确保不会丢帧。
可以通过打印每次read的耗时来评估缓冲区是否合适,如果耗时波动很大,说明缓冲区偏小。
7. 从PCM到可用数据:截取之后的处理链路
拿到PCM数据只是第一步,后面还有很多事情要做。这一节简单讲讲数据处理的常见链路。
7.1 PCM数据的格式与转换
AudioRecord读出来的通常是16位PCM,每个采样点用short表示。如果你要做FFT分析,需要先转成float或double数组,并做归一化:
float[] normalized = new float[buffer.length]; for (int i = 0; i < buffer.length; i++) { normalized[i] = buffer[i] / 32768.0f; }如果是立体声,还需要做声道分离或者混合。做频谱分析时通常混合成单声道就够了。
7.2 实时频谱分析的实现要点
做实时频谱分析,核心是FFT。Android上可以用一些现成的库,也可以自己实现。关键点在于:
- 加窗:直接对原始数据做FFT会有频谱泄漏,需要加汉宁窗或汉明窗。
- 分帧:不能对整段音频做FFT,要分成小帧(比如1024或2048个采样点)逐帧处理。
- 重叠:相邻帧之间要有重叠(通常50%),避免帧边界处的信息丢失。
这些细节决定了频谱图的质量,做可视化的话尤其要注意。
7.3 数据落盘与回放验证
调试阶段,建议把捕获到的PCM数据直接写成WAV文件,然后用播放器回放验证。这样能快速判断捕获是否正常,比盯着频谱图猜要高效得多。WAV头部的构造不复杂,网上有很多现成的工具类可以用。
8. 性能与功耗的平衡:长时间捕获的优化经验
如果只是短时间捕获,性能问题不大。但如果要做长时间的音频捕获和分析,功耗和性能就会成为瓶颈。
8.1 线程模型的设计
音频读取必须放在独立的高优先级线程里,不能放在主线程。同时,数据处理(比如FFT)如果比较耗时,也应该放到单独的线程,避免阻塞读取。我通常用两个线程:一个专门读,一个专门算,中间用环形缓冲区传递数据。
8.2 采样率与精度的取舍
采样率越高,数据量越大,功耗也越高。如果只是做低频的节拍检测,22050Hz甚至16000Hz就够了,没必要用44100Hz。根据实际需求选择最低可用的采样率,能显著降低功耗。
8.3 及时释放资源
AudioRecord和MediaProjection都是系统资源,用完必须及时释放。特别是MediaProjection,如果不释放,下次申请可能会失败。在onDestroy或捕获结束时,务必调用stop()和release()。
9. 一些实际项目中的经验体会
做了几个音频相关的项目之后,我最大的体会是:音频捕获这件事,方案选择比代码实现更重要。选错了方案,后面再怎么优化都是事倍功半。
如果只是做自己应用的可视化,老老实实用Visualizer或者AudioTrack复制,别去碰MediaProjection,那套授权流程对用户体验是很大的负担。如果确实需要捕获系统音频,那就要做好兼容性测试,多准备几台不同厂商的设备,因为ROM差异真的很大。
还有一点,音频数据的实时处理对代码质量要求很高,一个小的内存分配或者锁竞争都可能导致卡顿。建议在开发阶段就打开性能分析工具,盯着CPU和内存曲线看,别等到上线了才发现问题。
最后分享一个小技巧:调试音频问题时,把中间数据写成WAV文件是最有效的排查手段。频谱图看着不对,你很难判断是捕获的问题还是分析的问题,但一听WAV文件就全明白了。这个习惯帮我省下了大量排查时间。