
1. 项目概述为什么要在Android SDK层面支持ADB录音在Android应用开发、系统测试乃至安全研究领域通过ADBAndroid Debug Bridge从设备上获取音频数据是一个高频且刚性的需求。无论是分析应用内音效、录制游戏过程、进行自动化语音测试还是排查音频相关的系统问题直接通过命令行抓取音频流远比在设备上安装一个录音App再导出文件要高效和纯净得多。然而原生Android SDK提供的ADB工具链中并没有一个开箱即用的、稳定的录音命令。adb shell里你能找到screenrecord来录屏但翻遍文档也找不到一个官方的audiorecord命令。这迫使开发者们各显神通有的推送可执行文件到设备有的依赖特定厂商的调试工具流程繁琐且兼容性差。这个项目的核心目标就是填补这个空白修改Android SDK的构建系统或工具链使其能够编译并集成一个可以通过ADB直接调用的、系统级的录音命令行工具。最终我们希望实现类似adb shell tinycap /sdcard/test.wav这样的简洁命令就能在PC端直接获得设备麦克风或系统音频的录制文件。这不仅仅是添加一个功能更是对Android调试能力的一次重要增强让音频调试变得和截图、录屏一样简单直接。对于应用开发者、测试工程师和系统集成商来说这意味着更流畅的工作流和更强大的问题排查手段。2. 核心思路与方案选型从“外挂”到“内置”的演进在动手修改SDK之前我们得先看看社区里大家是怎么解决这个问题的。主流做法无外乎以下几种但各有各的痛点使用tinymix/tinycap等开源工具这是目前最流行的“外挂”方案。tinycap是 TinyALSA 项目里的一个工具可以直接从ALSA驱动层抓取PCM数据。我们需要先交叉编译它然后通过adb push放到设备的/data/local/tmp目录下再赋予执行权限。这种方法的问题是每次测试新设备或系统升级后都可能需要重新编译适配而且需要root权限才能访问某些音频设备节点对普通开发机不友好。利用MediaRecorder的AmActivity Manager命令可以通过adb shell am start等命令间接调用系统录音服务但这种方式无法实时获取音频流且控制粒度很粗不适合精细化的调试场景。自行编写一个包含Native代码的Android应用将录音逻辑写在JNI里打包成APK安装执行。这比第一种方法更“Android”一些但依然需要安装和启动应用过程冗长且无法做到命令行那样的即时性和脚本化集成。分析以上方案痛点集中在部署繁琐、权限要求高和体验不原生上。因此我们的思路很明确为什么不把这个工具直接做到SDK里去让它在编译系统镜像尤其是userdebug或eng版本时就作为一个系统工具被编译进去像logcat、dumpsys一样随时待命。方案选型上我们决定基于tinycap进行改造和集成原因有三轻量高效tinycap代码简洁功能纯粹就是读取音频设备节点并写入文件依赖少非常适合作为系统工具。贴近底层它直接操作ALSA能捕获最原始的音频数据对于调试音频驱动、排查数据流问题非常有价值。社区验证方案经过大量开发者实际使用稳定性和可靠性有保障。我们要做的不是从零造轮子而是让这个好用的轮子成为Android官方工具链的“标配”。3. 深入Android SDK构建系统找到集成入口Android的构建系统Soong/Bazel庞大而复杂盲目修改只会迷路。我们的目标是让tinycap成为SDK的一部分这意味着我们需要找到一个地方让它能够被系统构建规则所识别、编译并最终打包到系统镜像的特定分区通常是system/bin中。3.1 定位系统工具所在目录首先我们需要熟悉Android源码中类似命令行工具的存放位置。打开你的AOSPAndroid Open Source Project源码目录系统自带的命令行工具主要存放在以下几个地方system/core/ 这里存放着最核心的工具比如toolbox、logcat。但这里工具通常非常基础与系统核心服务紧密耦合。frameworks/base/cmds/这是我们的重点关注目录许多系统命令都住在这里例如am、pm、wm等。这些命令通常作为系统服务如ActivityManager, PackageManager的客户端存在。external/ 存放所有第三方开源项目比如tinyalsatinycap所在项目原本就放在这里。我们的策略是不直接修改external/tinyalsa/而是为tinycap在frameworks/base/cmds/下创建一个新的“家”。这样做的好处是它更符合Android系统命令的组织规范便于维护也更容易被构建系统识别为需要集成到镜像中的目标。3.2 理解Blueprint (Soong)或Android.mk构建文件Android的构建系统经历了从Android.mk到Android.bp(Blueprint/Soong) 的演变。新项目推荐使用Android.bp。我们需要创建一个构建文件告诉构建系统“这里有一个可执行程序需要编译”。假设我们在frameworks/base/cmds/下创建了一个新目录tinycap/。那么在这个目录里我们需要创建Android.bp文件。// frameworks/base/cmds/tinycap/Android.bp cc_binary { name: tinycap, srcs: [tinycap.c], shared_libs: [ libcutils, liblog, ], cflags: [ -Wall, -Werror, ], init_rc: [tinycap.rc], // 可选如果需要作为服务 vendor: true, // 根据情况决定是放在system还是vendor分区 }这段构建脚本定义了一个名为tinycap的C/C可执行文件源文件是tinycap.c它链接了libcutils和liblog这两个Android基础库。cflags指定了编译选项。注意这里我们假设将tinycap.c的源码复制到了这个目录。更工程化的做法是通过cc_library_static先编译external/tinyalsa为静态库然后在这里链接。但为了简化集成步骤直接复制源码并适配是最快的方式。你需要确保源码中的头文件引用如tinyalsa/asoundlib.h路径是正确的可能需要修改为#include external/tinyalsa/include/tinyalsa/asoundlib.h或直接将头文件也复制过来。3.3 修改系统镜像编译配置仅仅定义了可执行文件还不够我们需要告诉构建系统在编译system.img或vendor.img时把这个程序打包进去。这通常是通过修改产品的配置文件.mk文件来实现的。找到你的目标设备的产品配置文件例如device/厂商/产品/产品名.mk或device/厂商/产品/system.prop。我们需要在其中添加PRODUCT_PACKAGES变量。# 在 device/xxx/xxx/xxx.mk 文件中添加 PRODUCT_PACKAGES \ tinycapPRODUCT_PACKAGES变量列出了所有需要被包含进最终系统镜像中的模块名。添加tinycap后构建系统在编译时就会去查找名为tinycap的模块即我们在Android.bp里定义的cc_binary并将其二进制文件放入相应的镜像中。4. 改造tinycap源码适配Android系统环境从external/tinyalsa拿到的tinycap.c是通用的Linux ALSA工具直接编译可能无法在Android上完美运行或者缺少我们想要的特性比如指定采样率、通道数、通过ADB参数控制。因此适度的改造是必须的。4.1 核心参数解析与默认值设定原版tinycap通常通过命令行参数指定文件名和设备号。我们希望增强它的实用性例如支持常见的音频格式参数。我们需要修改main函数开头的参数解析逻辑。// ... 省略头文件 ... int main(int argc, char **argv) { const char *file_path NULL; unsigned int card 0; unsigned int device 0; unsigned int channels 2; // 默认立体声 unsigned int rate 48000; // 默认48kHz采样率 unsigned int bits 16; // 默认16位采样深度 unsigned int period_size 1024; unsigned int period_count 4; unsigned int capture_duration 0; // 默认无限录制直到CtrlC int c; while ((c getopt(argc, argv, c:r:b:C:D:d:t:h)) ! -1) { switch (c) { case c: channels atoi(optarg); break; case r: rate atoi(optarg); break; case b: bits atoi(optarg); break; case C: card atoi(optarg); break; case D: device atoi(optarg); break; case d: capture_duration atoi(optarg); // 录制时长单位秒 break; case t: period_size atoi(optarg); break; case h: default: usage(); return EXIT_FAILURE; } } if (optind argc) { fprintf(stderr, Error: Output file path is required.\n); usage(); return EXIT_FAILURE; } file_path argv[optind]; // ... 后续录制逻辑 ... }同时需要实现一个usage()函数来打印帮助信息让用户知道如何使用这些新参数。4.2 关键录制逻辑与Android权限适配录制逻辑的核心是配置PCM参数并进入读写循环。这里需要注意Android上的权限问题。访问类似/dev/snd/pcmC0D0c这样的ALSA设备节点通常需要root权限或audio用户组权限。struct pcm_config config { .channels channels, .rate rate, .period_size period_size, .period_count period_count, .format bits 16 ? PCM_FORMAT_S16_LE : PCM_FORMAT_S32_LE, // 根据bits选择格式 .start_threshold 0, .stop_threshold 0, .silence_threshold 0, }; struct pcm *pcm pcm_open(card, device, PCM_IN, config); if (!pcm || !pcm_is_ready(pcm)) { fprintf(stderr, Unable to open PCM device (%s)\n, pcm_get_error(pcm)); return EXIT_FAILURE; } FILE *file fopen(file_path, wb); if (!file) { fprintf(stderr, Unable to create file %s\n, file_path); pcm_close(pcm); return EXIT_FAILURE; } // 写入WAV文件头如果需要WAV格式这是一个非常实用的增强 write_wav_header(file, rate, channels, bits); size_t buffer_size pcm_frames_to_bytes(pcm, period_size); void *buffer malloc(buffer_size); unsigned long long frames_captured 0; unsigned long long max_frames capture_duration * rate; // 计算最大帧数 while (1) { if (pcm_read(pcm, buffer, buffer_size)) { fprintf(stderr, Error reading PCM data.\n); break; } size_t written fwrite(buffer, 1, buffer_size, file); if (written ! buffer_size) { fprintf(stderr, Error writing to file.\n); break; } frames_captured pcm_bytes_to_frames(pcm, buffer_size); // 如果设置了录制时长检查是否超时 if (capture_duration 0 frames_captured max_frames) { printf(Capture duration reached.\n); break; } // 检查用户是否发送了中断信号如CtrlC if (g_interrupted) { printf(Interrupted by user.\n); break; } }实操心得关于WAV文件头。直接录制PCM数据生成的是裸流.pcm或.raw很多播放器无法直接播放。在工具内部集成一个简单的WAV文件头写入函数write_wav_header会极大提升易用性。这个函数根据采样率、通道数、位深计算并写入标准的44字节WAV头信息。这样生成的.wav文件可以被任何播放器识别。4.3 信号处理与资源释放一个健壮的命令行工具必须优雅地处理用户中断SIGINT, 即CtrlC。我们需要在程序开始时设置信号处理器。#include signal.h volatile sig_atomic_t g_interrupted 0; void handle_signal(int sig) { g_interrupted 1; } // 在main函数中开始录制前注册 signal(SIGINT, handle_signal); signal(SIGTERM, handle_signal);在录制循环中我们已检查g_interrupted变量。循环结束后必须确保所有资源被正确关闭和释放。free(buffer); pcm_close(pcm); // 在文件末尾更新WAV头中的文件大小信息如果写了WAV头 if (file) { update_wav_header(file, frames_captured); // 需要另一个函数来seek回文件头并更新size fclose(file); } printf(Capture finished. Total frames: %llu\n, frames_captured); return EXIT_SUCCESS;5. 编译、刷机与集成验证代码和构建脚本准备好后就到了验证环节。这个过程需要与Android源码的完整编译流程结合。5.1 整编源码并刷机初始化构建环境在AOSP根目录执行source build/envsetup.sh然后lunch选择你的目标设备必须是userdebug或eng版本user版本通常无法添加系统工具。执行编译使用m tinycap可以单独编译我们的模块。但更稳妥的方式是重新编译整个系统镜像make -j$(nproc)。构建系统会根据PRODUCT_PACKAGES的配置将tinycap编译并链接到镜像中。刷入设备编译完成后使用fastboot flashall -w或设备特定的刷机命令将新的system.img等镜像刷入设备。务必提前备份数据。5.2 ADB连接与功能测试设备重启后通过ADB连接并进入shell验证工具是否集成成功。adb root # 获取root权限userdebug/eng版本可行 adb shell which tinycap # 查看命令是否存在 ls -l /system/bin/tinycap # 查看文件路径和权限 tinycap --help # 测试帮助信息是否正常显示接下来进行实际的录音测试。首先需要确定音频设备号。在Android上音频设备映射关系可能因芯片和内核而异。# 查看可用的PCM设备列表 cat /proc/asound/pcm # 或者使用tinymix如果也有集成查看 tinymix -D 0 # 查看card 0的设备和控制项假设我们要录制主麦克风card 0, device 0保存到SD卡格式为48kHz 16bit 立体声录制10秒。tinycap -C 0 -D 0 -c 2 -r 48000 -b 16 -d 10 /sdcard/Music/test_capture.wav执行命令后观察命令行输出检查是否有错误信息。同时可以在设备上播放一些声音或对着麦克风说话。10秒后命令自动结束。将文件拉取到电脑上播放验证。adb pull /sdcard/Music/test_capture.wav .5.3 常见问题与排查技巧实录在实际集成和测试过程中你几乎一定会遇到下面这些问题。这里记录了我的排查实录问题1编译失败提示找不到tinyalsa/asoundlib.h头文件。现象fatal error: tinyalsa/asoundlib.h: No such file or directory排查这是头文件路径问题。我们的Android.bp没有正确指定头文件包含路径。解决在Android.bp中添加local_include_dirs或include_dirs字段指向external/tinyalsa/include。或者更彻底的方式是将必要的头文件tinyalsa/asoundlib.h及其依赖复制到我们项目的本地include/目录下并修改源码中的#include语句为#include “asoundlib.h”。我推荐后者它减少了模块间的耦合。问题2ADB执行tinycap时报错Permission denied。现象Unable to open PCM device (Permission denied)排查首先检查ls -l /dev/snd/pcmC0D0c的设备节点权限。在Android上这些节点通常属于audio用户组。解决确保ADB有root权限adb root。修改SELinux策略这是更深层的原因。在userdebug版本中你可以临时禁用SELinux来测试adb shell setenforce 0。如果禁用后功能正常说明需要添加SELinux策略。这涉及到修改file_contexts和te文件例如在device/厂商/产品/sepolicy/下为tinycap域添加允许访问音频设备节点的规则。这是一个高级主题但对于系统工具集成是必须跨越的一步。问题3录制出来的WAV文件播放速度异常太快或太慢或者全是噪音。现象文件能播放但声音尖细像快进或低沉像慢放或者根本不是录制的声音。排查这几乎总是PCM参数不匹配导致的。播放器按照WAV头里的参数采样率、位深、通道数播放但实际写入的数据是按照另一套参数生成的。解决仔细核对确保tinycap命令中指定的-c、-r、-b参数与设备音频源的实际能力匹配。可以通过cat /proc/asound/pcm查看设备支持的格式列表。检查WAV头写入逻辑确保write_wav_header和update_wav_header函数中计算数据大小、字节率等字段的公式完全正确。一个字节算错都会导致播放异常。可以先用一个已知正确的WAV文件做二进制对比。尝试裸数据暂时注释掉WAV头写入和更新的代码直接录制.raw文件。然后用专业的音频编辑软件如Audacity以你设定的参数导入原始数据如果能正确播放问题就出在WAV头处理上。问题4命令在后台持续录制但无法用CtrlC在adb shell中中断。现象按下CtrlC命令没有反应录制继续。排查ADB Shell 的信号传递可能有问题或者我们的信号处理函数没有被正确调用。解决在代码中增加更详细的日志打印信号是否被捕获。尝试使用kill命令从另一个ADB Shell窗口发送SIGINTadb shell kill -INT $(pidof tinycap)。作为备选方案实现一个超时机制我们已通过-d参数实现是更可靠的做法避免进程失控。6. 进阶优化与扩展思路基础功能稳定后可以考虑以下优化让这个工具更加强大和易用。6.1 支持录制系统内部音频非麦克风默认情况下tinycap录制的是捕获路径Capture Path的音频通常是麦克风。要录制系统播放的音频如媒体声音需要访问播放路径Playback Path的回路Loopback设备。这高度依赖于硬件和内核驱动是否支持。在一些设备上可能存在名为“Loopback”、“Mix”或特定数字的PCM设备用于回路捕获。你需要查阅设备的内核音频驱动文档或通过tinymix工具探索混音器路由将播放音频路由到某个可被捕获的PCM设备上。这涉及到复杂的音频链路配置通常不是通用方案。6.2 封装为更友好的ADB命令目前我们需要先adb shell再执行tinycap。可以更进一步在主机端的ADB工具中或创建一个独立的脚本封装一个直接可用的命令例如adb record-audio。这需要修改主机端的ADB客户端代码在AOSP的system/core/adb/目录下添加一个新的服务命令。当在PC端执行adb record-audio file.wav时ADB客户端会向设备端的ADB守护进程发送一个特定的请求守护进程再启动tinycap并将音频数据流通过USB/Socket实时传输回主机保存。这实现了真正的“一键录音”体验与adb shell screencap一致是项目的终极形态。6.3 集成到Android Studio或其它IDE对于应用开发者如果能像“布局检查器”或“网络分析器”一样在Android Studio中有一个图形化的音频录制面板将会非常方便。这需要开发一个Android Studio插件。插件通过ADB与设备通信发送命令控制tinycap的启停并拉取音频文件在IDE内提供简单的播放、保存和分析功能。这虽然工作量较大但能极大提升开发体验将调试工具无缝融入开发环境。整个项目从分析需求、选型方案、修改SDK、适配源码、解决编译和权限问题到最终测试验证是一个典型的Android系统级功能开发流程。它不仅仅关乎一个录音命令更是一次对Android构建系统、权限模型、驱动交互和工具链设计的深入实践。当你成功运行adb shell tinycap并听到清晰的录音回放时你会感受到这种底层集成的力量——它让一个复杂的需求变成了一个简单而强大的命令。