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用户就能感觉到。

我总结了几条实战经验:

  1. 使用AAudio的独占模式AAUDIO_PERFORMANCE_MODE_LOW_LATENCY会尝试独占音频硬件,绕过系统混音器。延迟可以从30ms降到5ms。
  2. 选择合适的缓冲区大小:缓冲区越小,延迟越低,但CPU负载越高。我一般从256帧开始调,根据设备性能上下调整。
  3. 使用回调而非轮询:AAudio的回调模式(AAudioStreamBuilder_setDataCallback)比手动读写更高效,因为它由音频驱动直接触发。
  4. 避免在音频线程做耗时操作:音频回调线程是实时线程,不能做内存分配、文件IO、网络请求。我曾经因为在一个回调里写日志文件,导致音频卡顿,查了两天才发现。
  5. 考虑USB传输的“帧周期”:USB音频传输是等时传输(Isochronous),每1ms传输一帧数据。理论上延迟下限就是1ms,但实际上受驱动和硬件影响,能做到3-5ms已经很不错了。

一个小技巧:如果你发现USB音频设备有“噗噗”的爆音,很可能是缓冲区太小导致XRun(欠载或过载)。试着把缓冲区增大一倍,通常能解决问题。

9.6 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

USB音频设备开发知识体系 UAC协议 UAC 1.0 UAC 2.0 Android支持 AAudio(推荐) OpenSL ES(兼容) 设备枚举与路由 采集与播放循环 延迟优化技巧 核心目标:低延迟、高兼容、稳定可靠的USB音频体验

这张图把本章的内容串起来了。从底层的UAC协议,到上层的API选择,再到具体的实战技巧,层层递进。

好了,USB音频开发的核心内容就这些。说实话,这部分内容在Android开发中属于比较小众但很实用的领域。如果你能把USB音频玩转,做直播App、K歌软件、甚至专业的音频处理工具都不在话下。

一句话总结:UAC协议是基础,AAudio是首选,延迟优化是灵魂。把这三点吃透,USB音频开发你就入门了。


公众号:蓝海资料掘金营,微信deep3321