9、USB音频设备开发:UAC协议与Android端实战
好,咱们进入USB开发中一个很有意思的领域——音频设备开发。说白了,就是让你的Android设备能接USB麦克风、USB音箱,甚至专业的音频接口。
我个人觉得,这部分内容特别适合那些想做直播设备、K歌软件、或者专业录音App的开发者。嗯,咱们一步步来。
9.1 UAC协议概述——USB音频设备的“通用语言”
UAC,全称USB Audio Class,是USB-IF组织定义的一套标准协议。它规定了USB音频设备(麦克风、音箱、声卡)和主机之间怎么通信。
为什么要搞这么个协议?你想想看,如果没有统一标准,每个厂商都自己定义一套指令,那Android系统得适配多少驱动?所以UAC就是那个“通用翻译器”。
核心要点:UAC协议定义了音频数据的格式、采样率、位深、通道数,以及控制命令(音量、静音、采样率切换等)。
目前主流的是UAC 1.0和UAC 2.0。我简单对比一下:
| 特性 | UAC 1.0 | UAC 2.0 |
|---|---|---|
| 带宽 | 最高48kHz/16bit/2ch | 最高192kHz/32bit/多通道 |
| 延迟 | 较高(10-30ms) | 较低(2-10ms) |
| Android原生支持 | Android 3.1+ | Android 10+(部分厂商) |
| 典型设备 | 普通USB耳机、廉价麦克风 | 专业声卡、高保真DAC |
我在项目中遇到过一个问题:某款USB麦克风在Android 9上能识别,但到了Android 11反而没声音。查了半天,发现是UAC版本协商出了问题。嗯,这里要注意——不同Android版本对UAC的支持深度不一样。
9.2 Android端USB声卡支持——系统怎么“认”出你的设备
Android系统对USB音频设备的支持,其实经历了一个漫长的过程。早期版本(Android 2.x)基本不支持,到了3.1才开始引入UAC 1.0的支持。
现在的情况是:
- Android 5.0+:原生支持UAC 1.0,大部分USB耳机、麦克风即插即用
- Android 10+:开始支持UAC 2.0,但需要硬件和驱动配合
- Android 12+:对低延迟音频有了更好的支持
但说实话,这里有个坑——很多国产手机厂商会魔改音频框架。我曾经遇到一台某品牌的旗舰机,插上USB声卡后系统能识别,但声音就是出不来。最后发现是厂商把USB音频的权限给禁用了。
避坑指南:如果你发现USB音频设备在某个手机上不工作,先检查系统设置里有没有“USB音频设备”的开关。有些手机需要手动开启。
9.3 音频API选择:AAudio vs OpenSL ES
好,现在设备识别了,怎么在App里读写音频数据?Android提供了两套主要的API:
9.3.1 OpenSL ES——老牌劲旅
OpenSL ES是Android早期引入的C语言音频API。它功能强大,但接口设计比较“原始”。
我个人习惯用OpenSL ES做简单的音频播放和录制,因为它稳定、兼容性好。但缺点也很明显:
- API设计复杂,回调机制不够灵活
- 延迟控制能力有限,通常20-50ms
- 对USB音频设备的支持不够完善
// OpenSL ES创建播放器的核心代码(简化版)
SLObjectItf engineObject;
SLEngineItf engineEngine;
SLObjectItf outputMixObject;
SLObjectItf playerObject;
// 创建引擎
slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
// 创建输出混音器
(*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, NULL, NULL);
(*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
// 配置音频源(这里可以指定USB设备)
SLDataLocator_AndroidSimpleBufferQueue locBufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
SLDataFormat_PCM formatPcm = {SL_DATAFORMAT_PCM, 2, SL_SAMPLINGRATE_48,
SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, SL_BYTEORDER_LITTLEENDIAN};
SLDataSource audioSrc = {&locBufq, &formatPcm};
// ... 后续创建播放器
9.3.2 AAudio——新生代低延迟方案
AAudio是Android 8.0引入的现代音频API。它专门为低延迟场景设计,接口更简洁。
我强烈建议新项目用AAudio。为什么?
- 延迟可以做到5-10ms,甚至更低
- API设计更符合现代C++风格
- 对USB音频设备有更好的支持
- 支持独占模式(Exclusive Mode),绕过混音器直接访问硬件
// AAudio创建音频流的核心代码
AAudioStreamBuilder *builder;
AAudioStream *stream;
// 创建Builder
AAudio_createStreamBuilder(&builder);
// 配置参数
AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_OUTPUT);
AAudioStreamBuilder_setSampleRate(builder, 48000);
AAudioStreamBuilder_setChannelCount(builder, 2);
AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_I16);
AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
// 打开流
AAudioStreamBuilder_openStream(builder, &stream);
// 开始播放
AAudioStream_requestStart(stream);
// 写入数据
AAudioStream_write(stream, audioData, numFrames, timeoutNanos);
// 清理
AAudioStream_close(stream);
AAudioStreamBuilder_delete(builder);
我的建议:如果目标设备是Android 8.0以上,优先用AAudio。如果必须兼容老设备,用OpenSL ES做降级方案。
9.4 USB麦克风/音箱的采集与播放实战
理论说完了,咱们来点实际的。怎么在代码里操作USB音频设备?
9.4.1 枚举USB音频设备
首先,你得知道系统里有哪些USB音频设备。Android提供了AudioManager来获取设备列表:
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
for (AudioDeviceInfo device : devices) {
if (device.getType() == AudioDeviceInfo.TYPE_USB_HEADSET ||
device.getType() == AudioDeviceInfo.TYPE_USB_DEVICE) {
Log.d("USB_AUDIO", "发现USB音频设备: " + device.getProductName());
Log.d("USB_AUDIO", " 采样率: " + Arrays.toString(device.getSampleRates()));
Log.d("USB_AUDIO", " 通道数: " + Arrays.toString(device.getChannelCounts()));
}
}
9.4.2 选择正确的设备进行采集/播放
拿到设备列表后,你需要把音频流路由到正确的设备上。AAudio支持通过deviceId指定设备:
// 找到USB麦克风的deviceId
int usbMicId = -1;
AudioDeviceInfo[] inputDevices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
for (AudioDeviceInfo device : inputDevices) {
if (device.getType() == AudioDeviceInfo.TYPE_USB_DEVICE) {
usbMicId = device.getId();
break;
}
}
// 用AAudio打开这个设备
AAudioStreamBuilder_setDeviceId(builder, usbMicId);
AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_INPUT);
嗯,这里有个细节要注意——有些USB麦克风支持多种采样率,但AAudio可能只支持其中一种。我建议你优先用48000Hz,兼容性最好。
9.4.3 完整的采集-播放循环
下面是一个简单的USB麦克风采集并实时播放到USB音箱的例子:
// 伪代码,展示核心逻辑
class UsbAudioLoopback {
AAudioStream *inputStream;
AAudioStream *outputStream;
void start() {
// 打开输入流(USB麦克风)
AAudioStreamBuilder *inputBuilder;
AAudio_createStreamBuilder(&inputBuilder);
AAudioStreamBuilder_setDirection(inputBuilder, AAUDIO_DIRECTION_INPUT);
AAudioStreamBuilder_setDeviceId(inputBuilder, usbMicId);
AAudioStreamBuilder_setPerformanceMode(inputBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_openStream(inputBuilder, &inputStream);
// 打开输出流(USB音箱)
AAudioStreamBuilder *outputBuilder;
AAudio_createStreamBuilder(&outputBuilder);
AAudioStreamBuilder_setDirection(outputBuilder, AAUDIO_DIRECTION_OUTPUT);
AAudioStreamBuilder_setDeviceId(outputBuilder, usbSpeakerId);
AAudioStreamBuilder_setPerformanceMode(outputBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_openStream(outputBuilder, &outputStream);
// 启动流
AAudioStream_requestStart(inputStream);
AAudioStream_requestStart(outputStream);
// 循环读写
const int bufferSize = 256; // 小缓冲区降低延迟
int16_t buffer[bufferSize];
while (isRunning) {
int64_t timeout = 1000000; // 1ms超时
int32_t numRead = AAudioStream_read(inputStream, buffer, bufferSize, timeout);
if (numRead > 0) {
AAudioStream_write(outputStream, buffer, numRead, timeout);
}
}
}
}
注意:上面的循环读写是同步方式,实际项目中建议用回调模式(AAudioStreamBuilder_setDataCallback),性能更好。
9.5 延迟优化技巧——让声音“零延迟”
做USB音频开发,延迟是绕不开的话题。尤其是做K歌、直播、乐器效果器这类应用,延迟超过20ms用户就能感觉到。
我总结了几条实战经验:
- 使用AAudio的独占模式:
AAUDIO_PERFORMANCE_MODE_LOW_LATENCY会尝试独占音频硬件,绕过系统混音器。延迟可以从30ms降到5ms。 - 选择合适的缓冲区大小:缓冲区越小,延迟越低,但CPU负载越高。我一般从256帧开始调,根据设备性能上下调整。
- 使用回调而非轮询:AAudio的回调模式(
AAudioStreamBuilder_setDataCallback)比手动读写更高效,因为它由音频驱动直接触发。 - 避免在音频线程做耗时操作:音频回调线程是实时线程,不能做内存分配、文件IO、网络请求。我曾经因为在一个回调里写日志文件,导致音频卡顿,查了两天才发现。
- 考虑USB传输的“帧周期”:USB音频传输是等时传输(Isochronous),每1ms传输一帧数据。理论上延迟下限就是1ms,但实际上受驱动和硬件影响,能做到3-5ms已经很不错了。
一个小技巧:如果你发现USB音频设备有“噗噗”的爆音,很可能是缓冲区太小导致XRun(欠载或过载)。试着把缓冲区增大一倍,通常能解决问题。
9.6 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图:
这张图把本章的内容串起来了。从底层的UAC协议,到上层的API选择,再到具体的实战技巧,层层递进。
好了,USB音频开发的核心内容就这些。说实话,这部分内容在Android开发中属于比较小众但很实用的领域。如果你能把USB音频玩转,做直播App、K歌软件、甚至专业的音频处理工具都不在话下。
一句话总结:UAC协议是基础,AAudio是首选,延迟优化是灵魂。把这三点吃透,USB音频开发你就入门了。
公众号:蓝海资料掘金营,微信deep3321