3. 音频HAL接口规范:详解audio_hw_device、audio_stream_in/out等核心结构体及回调函数定义

Android音频HAL(Hardware Abstraction Layer)是连接AudioFlinger(系统音频服务)与底层音频硬件驱动的关键桥梁。在Android 8.0及之后的版本中,HAL接口从传统的hardware/libhardware迁移至hardware/interfaces/audio下的HIDL(HAL Interface Definition Language)定义,但核心的数据结构语义与回调函数设计思路仍然延续了经典C结构体的风格。本章将深入剖析audio_hw_device、audio_stream_in、audio_stream_out三个核心结构体的字段含义、回调函数指针的职责边界,并结合AOSP(Android Open Source Project)中的tinyalsa实现,讲解如何在实际驱动开发中正确实现这些接口。

音频HAL核心结构体关系与回调链路 AudioFlinger 系统音频策略管理者 audio_hw_device open/close/get_supported_devices create_stream_patch/set_voice_volume audio_stream_out write/get_latency/set_volume get_render_position audio_stream_in read/get_input_frames_lost set_gain/get_active_microphones 硬件驱动 / Codec / DSP I2S / TDM / SLIMbus / USB HAL_MODULE_INFO_SYM open_output_stream open_input_stream ALSA / DSP 驱动 关键回调函数 create_stream_patch set_parameters / get_parameters dump / set_master_volume 数据流方向 播放: App → AudioFlinger → HAL write() → 硬件

上图展示了音频HAL中三个核心结构体之间的协作关系。AudioFlinger通过audio_hw_device的open_output_stream和open_input_stream回调创建音频流对象,随后通过流对象的write/read回调进行实际的数据传输。理解这张图是掌握HAL开发的第一步,接下来我们将逐一拆解每个结构体的字段与回调。

3.1 audio_hw_device结构体:HAL模块的“总控制台”

3.1.1 核心原理:设备级抽象与生命周期管理

audio_hw_device是音频HAL中最高级别的抽象,代表一个完整的音频硬件设备(如SoC内部的音频控制器、外部Codec或USB声卡)。在AOSP的hardware/libhardware/include/hardware/audio.h中,其定义以audio_hw_device结构体呈现,包含两个部分:公共头hw_device_t common和一系列函数指针。该结构体的核心职责包括:

  • 设备生命周期管理:通过open和close回调控制硬件设备的初始化与释放。
  • 全局参数控制:如主音量(set_master_volume)、静音状态(set_master_mute)、模式切换(set_mode,如正常/通话/环回)。
  • 音频路径管理:Android 8.0后引入的create_stream_patch和release_stream_patch用于动态路由,替代了旧的set_route。
  • 能力查询:get_supported_devices返回设备支持的音频输出/输入设备位掩码(如AUDIO_DEVICE_OUT_SPEAKER、AUDIO_DEVICE_IN_MIC)。

在HIDL化之后,这些回调被映射到IDevice接口的HIDL方法,但底层实现仍然需要操作相同的硬件资源。对于HAL开发者而言,理解C结构体版本有助于阅读旧代码和调试兼容性问题。

3.1.2 关键字段与回调函数清单

字段/回调函数原型职责说明典型实现要点
commonhw_device_t common包含模块标签、版本号、close函数指针必须初始化tag=HARDWARE_DEVICE_TAG,版本设为AUDIO_DEVICE_API_VERSION_2_0
get_supported_devicesuint32_t (*get_supported_devices)(const struct audio_hw_device *dev)返回设备支持的所有输入/输出设备掩码根据硬件Codec的物理连接(如耳机、扬声器、麦克风)设置对应bit
open_output_streamint (*open_output_stream)(struct audio_hw_device *dev, audio_io_handle_t handle, audio_format_t *format, audio_channel_mask_t *channels, uint32_t *sample_rate, audio_output_flags_t flags, audio_offload_info_t *offload_info, const char *address, struct audio_stream_out **stream_out)创建输出流对象需要根据format、channels、sample_rate的输入值协商硬件能力,若硬件不支持则返回错误或修改参数
open_input_streamint (*open_input_stream)(struct audio_hw_device *dev, audio_io_handle_t handle, audio_format_t *format, audio_channel_mask_t *channels, uint32_t *sample_rate, audio_input_flags_t flags, const char *address, struct audio_stream_in **stream_in)创建输入流对象与输出流类似,但需额外考虑输入设备的增益控制和噪声抑制
set_master_volumeint (*set_master_volume)(struct audio_hw_device *dev, float volume)设置主音量(0.0~1.0)通常映射到Codec的数字音量寄存器,注意对数映射关系
create_stream_patchint (*create_stream_patch)(struct audio_hw_device *dev, struct audio_patch *patch)创建音频路由补丁根据patch中的源/宿设备选择I2S、TDM或DSP路由
dumpint (*dump)(struct audio_hw_device *dev, int fd)输出HAL状态信息到文件描述符用于dumpsys media.audio_flinger调试,需输出寄存器状态、流信息等

3.1.3 案例拆解:基于tinyalsa的open_output_stream实现

以AOSP中hardware/libhardware/modules/audio/audio_hw.c为例,open_output_stream的实现逻辑如下:

static int out_open(struct audio_hw_device *dev, audio_io_handle_t handle,
                    audio_format_t *format, audio_channel_mask_t *channels,
                    uint32_t *sample_rate, audio_output_flags_t flags,
                    audio_offload_info_t *offload_info, const char *address,
                    struct audio_stream_out **stream_out) {
    struct audio_device *adev = (struct audio_device *)dev;
    struct stream_out *out;
    int ret;
    // 1. 分配流对象内存
    out = calloc(1, sizeof(struct stream_out));
    if (!out) return -ENOMEM;
    // 2. 协商采样率:若请求的采样率硬件不支持,则回退到默认值
    if (*sample_rate != 48000 && *sample_rate != 44100) {
        *sample_rate = 48000;  // 修改调用方参数
    }
    // 3. 协商通道数:仅支持立体声或单声道
    if (*channels != AUDIO_CHANNEL_OUT_STEREO && *channels != AUDIO_CHANNEL_OUT_MONO) {
        *channels = AUDIO_CHANNEL_OUT_STEREO;
    }
    // 4. 协商格式:仅支持PCM 16bit
    if (*format != AUDIO_FORMAT_PCM_16_BIT) {
        *format = AUDIO_FORMAT_PCM_16_BIT;
    }
    // 5. 初始化ALSA设备(tinyalsa接口)
    out->pcm = pcm_open(0, 0, PCM_OUT, &pcm_config);
    if (!pcm_is_ready(out->pcm)) {
        ret = pcm_close(out->pcm);
        free(out);
        return -EIO;
    }
    // 6. 设置流回调函数
    out->stream.common.get_sample_rate = out_get_sample_rate;
    out->stream.common.set_sample_rate = out_set_sample_rate;
    out->stream.write = out_write;
    out->stream.get_latency = out_get_latency;
    // 7. 返回流对象
    *stream_out = &out->stream;
    return 0;
}

这段代码展示了三个关键实践:参数协商(修改调用方传入的format/channels/sample_rate)、硬件初始化(通过tinyalsa打开PCM设备)、回调函数绑定。注意第2-4步中,HAL必须将不支持的参数修改为最接近的硬件能力值,而不是直接返回错误,这是Android音频框架的约定。

常见误区:许多初学者在open_output_stream中直接返回-EINVAL拒绝不支持的参数。这会导致AudioFlinger反复尝试不同的参数组合,甚至导致系统音频服务崩溃。正确的做法是尽量“降级”到硬件支持的最优参数,并在stream_out的get_sample_rate等回调中返回实际协商后的值。

3.2 audio_stream_out与audio_stream_in:数据通道的“双向门”

3.2.1 核心原理:流对象的职责边界

audio_stream_out和audio_stream_in分别代表播放和录音的数据通道。它们都包含一个公共头audio_stream_t common(包含get_sample_rate、set_sample_rate、get_buffer_size等基础回调),以及各自特有的回调。输出流的核心是write回调,输入流的核心是read回调。这两个回调是音频数据的“高速公路”,其性能直接决定音频延迟和卡顿率。

输出流的关键回调包括:

  • write:将音频数据写入硬件,阻塞直到数据被消费或超时。
  • get_latency:返回从写入到听到声音的延迟(毫秒),用于AudioFlinger的延迟补偿。
  • set_volume:设置流级别的音量(区别于设备主音量)。
  • get_render_position:返回硬件播放的帧位置,用于音画同步。

输入流的关键回调包括:

  • read:从硬件读取音频数据,阻塞直到数据可用。
  • get_input_frames_lost:返回录音过程中丢失的帧数,用于质量监控。
  • set_gain:设置输入增益(通常用于麦克风放大)。
  • get_active_microphones:返回当前激活的麦克风信息(用于多麦克风阵列)。

3.2.2 判断方法:如何正确实现write回调

write回调的实现质量是HAL性能的分水岭。以下是判断和实现的关键步骤:

  1. 检查缓冲区大小:write传入的buffer_size必须与get_buffer_size返回值一致,否则应返回-EINVAL。
  2. 处理部分写入:硬件可能只接受部分数据,此时应返回实际写入的字节数,而不是错误。
  3. 阻塞策略:对于同步I/O,应使用pcm_write阻塞直到数据完全写入;对于异步I/O,应使用非阻塞模式并配合事件循环。
  4. 静音处理:当系统静音时,HAL应继续消费数据但丢弃内容,避免音频管线卡死。
static ssize_t out_write(struct audio_stream_out *stream, const void *buffer,
                         size_t bytes) {
    struct stream_out *out = (struct stream_out *)stream;
    // 1. 检查参数
    if (bytes == 0 || bytes % out->frame_size != 0) {
        return -EINVAL;
    }
    // 2. 如果处于静音状态,填充静音数据
    if (out->mute) {
        memset((void *)buffer, 0, bytes);
    }
    // 3. 写入硬件(tinyalsa的pcm_write会阻塞)
    int ret = pcm_write(out->pcm, buffer, bytes);
    if (ret != 0) {
        // 处理XRUN(欠载)情况
        if (pcm_prepare(out->pcm) == 0) {
            return bytes;  // 恢复后假装写入成功
        }
        return -EIO;
    }
    return bytes;
}
经验提醒:在write回调中,务必处理XRUN(缓冲区欠载/溢出)情况。当pcm_write返回错误时,应先调用pcm_prepare恢复PCM设备,然后返回bytes(表示数据已被消费),避免AudioFlinger误判为硬件故障而停止播放。

3.2.3 案例拆解:多通道输入流的read实现与增益控制

假设某设备使用双麦克风阵列(主麦克风+参考麦克风)用于回声消除。输入流的read回调需要同时读取两个通道的数据,并允许上层通过set_gain调整主麦克风增益。实现要点如下:

static ssize_t in_read(struct audio_stream_in *stream, void *buffer,
                       size_t bytes) {
    struct stream_in *in = (struct stream_in *)stream;
    // 1. 从硬件读取原始数据(双通道交错)
    int ret = pcm_read(in->pcm, buffer, bytes);
    if (ret != 0) {
        if (pcm_prepare(in->pcm) == 0) {
            memset(buffer, 0, bytes);  // 返回静音数据
            return bytes;
        }
        return -EIO;
    }
    // 2. 应用增益(仅调整主麦克风通道)
    if (in->gain != 1.0f) {
        int16_t *samples = (int16_t *)buffer;
        int frame_count = bytes / (in->channels * 2);
        for (int i = 0; i < frame_count; i++) {
            samples[i * in->channels] = (int16_t)(samples[i * in->channels] * in->gain);
        }
    }
    return bytes;
}

这个案例展示了三个关键点:多通道数据处理(交错格式的索引计算)、增益控制(浮点乘法后截断)、错误恢复(返回静音数据而非错误)。在实际产品中,增益控制通常由DSP硬件完成,但软件实现有助于快速原型验证。

3.3 回调函数的同步机制与线程模型

3.3.1 核心原理:HAL回调的调用上下文

理解HAL回调的线程模型对避免死锁和竞态条件至关重要。AudioFlinger通常使用以下线程调用HAL回调:

  • 播放线程:调用write、get_render_position等输出流回调。
  • 录音线程:调用read、get_input_frames_lost等输入流回调。
  • 控制线程:调用set_parameters、set_volume等设备级回调。

这些线程可能同时访问HAL内部状态,因此HAL实现必须使用互斥锁(pthread_mutex_t)保护共享数据。例如,当set_volume被控制线程调用时,正在执行的write回调可能正在读取音量值,此时需要加锁。

3.3.2 判断方法:识别死锁风险点

以下场景是HAL开发中常见的死锁陷阱:

  1. 回调中调用其他回调:例如在write中调用set_parameters,而set_parameters又尝试获取write持有的锁。
  2. 长时间阻塞:write回调中执行超过100ms的操作(如I2C寄存器读写),会阻塞AudioFlinger的播放线程,导致系统音频卡顿。
  3. 锁顺序不一致:多个锁的获取顺序必须全局一致,否则可能产生ABBA死锁。
最佳实践:在HAL实现中,建议将耗时操作(如硬件寄存器配置)移出回调函数,使用工作队列或异步线程处理。例如,set_parameters回调只更新内部状态并发送消息,由专用线程执行实际的硬件配置。

3.3.3 案例拆解:set_parameters与write的并发处理

某HAL实现中,set_parameters需要切换音频路由(从扬声器切换到耳机),而write正在播放数据。错误实现可能导致路由切换时播放中断。正确做法如下:

static int out_set_parameters(struct audio_stream *stream, const char *kvpairs) {
    struct stream_out *out = (struct stream_out *)stream;
    pthread_mutex_lock(&out->lock);
    // 解析参数并更新路由标志
    out->routing = parse_routing(kvpairs);
    pthread_mutex_unlock(&out->lock);
    // 异步执行路由切换,避免阻塞控制线程
    send_route_change_message(out);
    return 0;
}

static ssize_t out_write(struct audio_stream_out *stream, const void *buffer,
                         size_t bytes) {
    struct stream_out *out = (struct stream_out *)stream;
    pthread_mutex_lock(&out->lock);
    // 检查是否有待处理的路由切换
    if (out->pending_route_change) {
        // 在播放间隙执行切换
        apply_route_change(out);
        out->pending_route_change = false;
    }
    pthread_mutex_unlock(&out->lock);
    // 继续写入数据
    return pcm_write(out->pcm, buffer, bytes);
}

这个案例展示了如何通过“延迟执行”策略避免回调间的竞争。在write回调中检查路由切换标志,并在播放间隙执行切换,既保证了控制线程不被阻塞,又避免了播放中断。

3.4 常见误区与调试技巧

3.4.1 误区清单

误区后果正确做法
在open_output_stream中直接返回错误AudioFlinger反复重试,系统音频服务崩溃协商参数到硬件支持的最优值
write回调中不处理XRUN播放卡顿或停止调用pcm_prepare恢复并返回bytes
忽略回调的线程模型数据竞争、死锁使用互斥锁保护共享状态
get_buffer_size返回值与实际不符AudioFlinger分配错误大小的缓冲区确保返回值为帧大小×通道数×采样率/1000的整数倍
在回调中执行长时间阻塞操作系统音频服务ANR使用异步线程处理耗时操作

3.4.2 调试技巧:使用dumpsys和systrace

当HAL出现问题时,首先使用adb shell dumpsys media.audio_flinger查看HAL状态。该命令会调用audio_hw_device的dump回调,输出HAL内部状态。其次,使用systrace抓取音频线程的调度情况,定位阻塞点。最后,在HAL代码中添加ALOGD日志,并通过adb logcat -s audio_hw_primary过滤查看。

风险提示:在dump回调中输出过多信息(如每次调用都打印寄存器值)会严重影响dumpsys的性能。建议仅在dump中输出关键状态(如当前路由、采样率、音量),并增加时间戳以便分析状态变化。

3.4.3 总结复盘:HAL接口设计的核心思想

回顾本章内容,音频HAL接口规范的设计体现了三个核心思想:参数协商(HAL与框架通过修改参数达成一致)、回调驱动(所有操作通过函数指针完成,便于多态实现)、职责分层(设备级与流级接口分离,支持多路并发)。掌握这些思想,不仅有助于实现正确的HAL,还能帮助理解Android音频框架的整体架构。在实际开发中,建议从最简单的单声道、44.1kHz、16bit配置开始,逐步增加对多通道、高采样率、低延迟模式的支持,每增加一个功能都要回归测试write/read的稳定性和延迟指标。