3、音频路由核心概念:音频流(Stream)、音频通道(Channel)、音频端点(Endpoint)、路由表(Routing Table)

好,咱们进入正题。音频路由,说白了就是决定「声音从哪里来,到哪里去」。在QNX系统里,这件事不是靠猜的,而是靠四个核心概念撑起来的:音频流(Stream)音频通道(Channel)音频端点(Endpoint)路由表(Routing Table)

我刚开始接触QNX音频架构时,也被这几个词绕晕过。后来发现,只要搞懂它们之间的关系,整个路由体系就清晰了。嗯,咱们一个一个来拆。

3.1 音频流(Stream)—— 声音的「载体」

音频流,你可以把它想象成一根水管。水(音频数据)在水管里流动,水管有方向,有流量,有格式。

在QNX中,一个Stream代表一个单向或双向的音频数据通道。它通常对应一个PCM数据流,比如你播放一首歌,或者录制一段语音。

我个人习惯把Stream分为三类:

  • 播放流(Playback Stream):数据从应用流向硬件,最终从喇叭出来。
  • 录制流(Capture Stream):数据从麦克风流向应用,存到文件或内存。
  • 全双工流(Duplex Stream):同时播放和录制,比如VoIP通话。

每个Stream都有自己独立的采样率、位深、通道数。举个例子:

// 创建一个播放流,44.1kHz,16位,立体声
io_stream_handle_t stream;
io_stream_attr_t attr = {
    .sample_rate = 44100,
    .bit_depth = 16,
    .num_channels = 2,
    .direction = IO_STREAM_DIRECTION_PLAYBACK
};
io_stream_create(&stream, &attr);

我在项目中遇到过一个问题:两个Stream同时播放,一个44.1kHz,一个48kHz,结果硬件不支持同时混音,直接报错。后来我加了一个采样率转换器(SRC)才搞定。所以,Stream的格式必须和硬件能力匹配,否则路由会失败。

避坑指南:我曾经在调试时发现Stream创建成功,但路由死活不通。查了半天,原来是Stream的方向设反了——播放流配成了录制方向。方向错了,路由表再漂亮也没用。

3.2 音频通道(Channel)—— 声音的「车道」

通道,就是Stream里面的「子车道」。一个立体声Stream有2个通道(左/右),一个5.1环绕声Stream有6个通道。

在QNX中,通道用索引号来标识:

  • 通道0:左前
  • 通道1:右前
  • 通道2:中置
  • 通道3:低音炮
  • 通道4:左后
  • 通道5:右后

你想想看,如果我要把左前声道的声音单独路由到左喇叭,右前声道路由到右喇叭,那就得操作通道级别的路由。

我建议你在设计多区声场时,把每个物理扬声器看作一个独立的通道目标。比如一个车内有6个喇叭,那就至少需要6个通道来分别控制。

// 获取Stream的第0个通道(左前)
io_channel_handle_t ch_left = io_stream_get_channel(stream, 0);
// 获取Stream的第1个通道(右前)
io_channel_handle_t ch_right = io_stream_get_channel(stream, 1);

这里有个容易踩的坑:通道编号是从0开始的。我曾经在代码里写了个循环,从1开始遍历通道,结果第一个通道永远没声音。嗯,低级错误,但很常见。

3.3 音频端点(Endpoint)—— 声音的「水龙头」

端点,就是音频数据的物理或逻辑出入口。它可以是:

  • 一个硬件Codec的I2S接口
  • 一个HDMI输出口
  • 一个蓝牙A2DP连接
  • 一个虚拟的环回设备

每个Endpoint都有自己的一组能力(Capabilities),比如支持的采样率列表、通道数上限、格式类型等。

在QNX中,Endpoint通常由io-audio驱动注册到系统中。你可以用io_audio_query_endpoints()来枚举所有可用的端点。

// 枚举所有音频端点
io_endpoint_info_t endpoints[10];
int count = io_audio_query_endpoints(endpoints, 10);
for (int i = 0; i < count; i++) {
    printf("Endpoint %d: %s, type=%d\n", 
           i, endpoints[i].name, endpoints[i].type);
}

我个人习惯把Endpoint分为三类:

类型 例子 特点
物理端点 喇叭、麦克风、Line In 直接对应硬件引脚
逻辑端点 ALSA设备、OSS设备 通过驱动抽象
虚拟端点 环回设备、混音器 用于内部数据流转

为什么要有虚拟端点?我在做多区声场时,经常需要把一路音频复制到多个物理端点。这时候,虚拟端点就像一个「分线器」,把数据分发给多个目标。

注意:Endpoint的命名规则因驱动而异。我曾经在换了一个Codec驱动后,原来的端点名全变了,路由表全部失效。所以,不要硬编码端点名,建议用查询方式动态获取。

3.4 路由表(Routing Table)—— 声音的「地图」

路由表,就是整个音频系统的交通指挥中心。它定义了:

  • 哪个Stream的哪个通道
  • 连接到哪个Endpoint的哪个通道
  • 以及连接的方向(播放/录制/双向)

在QNX中,路由表是一个矩阵结构。每一行是一个源,每一列是一个目标。交叉点上的值表示是否允许路由。

我举个例子,一个简单的两声道系统:

// 路由表配置:Stream0的通道0 -> EndpointA的通道0
//              Stream0的通道1 -> EndpointA的通道1
io_routing_entry_t route_table[] = {
    {
        .src_stream = stream0,
        .src_channel = 0,
        .dst_endpoint = endpointA,
        .dst_channel = 0,
        .direction = IO_ROUTE_DIRECTION_PLAYBACK
    },
    {
        .src_stream = stream0,
        .src_channel = 1,
        .dst_endpoint = endpointA,
        .dst_channel = 1,
        .direction = IO_ROUTE_DIRECTION_PLAYBACK
    }
};
io_routing_table_apply(route_table, 2);

路由表的核心原则是:一个源通道只能路由到一个目标通道,但一个目标通道可以接收多个源通道(混音)。

我在项目中遇到过最头疼的问题:路由表配置正确,但声音就是不出来。后来发现,路由表需要先「激活」才能生效。很多新手(包括当年的我)只配置了表,忘了调用io_routing_table_commit()

核心要点:路由表是动态的。你可以在运行时添加、删除、修改路由条目,而不需要重启音频服务。这就是QNX音频架构灵活的地方。

3.5 四个概念的关系

好,咱们捋一捋这四个概念的关系:

  1. Stream 是数据的生产者或消费者,它包含多个 Channel
  2. Channel 是Stream内部的子通道,每个通道可以独立路由。
  3. Endpoint 是物理或逻辑的音频接口,它也包含多个通道。
  4. Routing Table 定义了Stream的Channel到Endpoint的Channel的映射关系。

说白了,就是:Stream里的数据,通过路由表,送到Endpoint去播放或录制

你想想看,如果我要实现一个「后排乘客听导航,前排乘客听音乐」的多区声场,怎么做?

  • 创建两个Stream:一个导航流,一个音乐流。
  • 导航流路由到后排喇叭对应的Endpoint通道。
  • 音乐流路由到前排喇叭对应的Endpoint通道。
  • 路由表里把两个流的目标通道分开。

嗯,就是这么简单。但实际项目中,往往有几十个Stream和上百个Endpoint,路由表的管理就变得非常复杂。我建议你用工具把路由表可视化,比如画个矩阵图,一眼就能看出哪里冲突了。

我的习惯:每次修改路由表后,我都会用io_routing_table_dump()打印当前表内容,确认无误后再提交。这个习惯帮我避免了好几次线上事故。

好了,这一章的核心概念就讲到这里。下一章咱们会深入路由表的动态管理,包括如何在运行时切换路由、处理混音冲突等实战技巧。到时候我会分享一个我踩过的坑——路由表更新顺序错了,导致整个系统静音了10秒钟。嗯,那是个难忘的下午。