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的格式必须和硬件能力匹配,否则路由会失败。
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设备 | 通过驱动抽象 |
| 虚拟端点 | 环回设备、混音器 | 用于内部数据流转 |
为什么要有虚拟端点?我在做多区声场时,经常需要把一路音频复制到多个物理端点。这时候,虚拟端点就像一个「分线器」,把数据分发给多个目标。
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()。
3.5 四个概念的关系
好,咱们捋一捋这四个概念的关系:
- Stream 是数据的生产者或消费者,它包含多个 Channel。
- Channel 是Stream内部的子通道,每个通道可以独立路由。
- Endpoint 是物理或逻辑的音频接口,它也包含多个通道。
- Routing Table 定义了Stream的Channel到Endpoint的Channel的映射关系。
说白了,就是:Stream里的数据,通过路由表,送到Endpoint去播放或录制。
你想想看,如果我要实现一个「后排乘客听导航,前排乘客听音乐」的多区声场,怎么做?
- 创建两个Stream:一个导航流,一个音乐流。
- 导航流路由到后排喇叭对应的Endpoint通道。
- 音乐流路由到前排喇叭对应的Endpoint通道。
- 路由表里把两个流的目标通道分开。
嗯,就是这么简单。但实际项目中,往往有几十个Stream和上百个Endpoint,路由表的管理就变得非常复杂。我建议你用工具把路由表可视化,比如画个矩阵图,一眼就能看出哪里冲突了。
io_routing_table_dump()打印当前表内容,确认无误后再提交。这个习惯帮我避免了好几次线上事故。
好了,这一章的核心概念就讲到这里。下一章咱们会深入路由表的动态管理,包括如何在运行时切换路由、处理混音冲突等实战技巧。到时候我会分享一个我踩过的坑——路由表更新顺序错了,导致整个系统静音了10秒钟。嗯,那是个难忘的下午。