第31章 音频通道(Audio Channels)
开发者指南 Audio Channels 音频通道在 Voicebox 中如何实现音频输出路由 概述 音频通道用于将语音输出路由至不同的音频设备。这在多输出场景中非常实用,例如让不同的语音角色通过不同的扬声器或应用程序播放。
架构 * 通道 (Channel):一个命名的音频总线,可分配给特定的输出设备。 * 设备映射:将通道链接到操作系统(OS)的音频设备标识符。 * 配置文件映射:将语音配置文件(Voice Profile)与通道关联(支持多对多关系)。
数据模型 AudioChannel 表 ```python class AudioChannel(Base): __tablename__ = "audio_channels" id = Column(String, primary_key=True) name = Column(String, nullable=False) is_default = Column(Boolean, default=False) created_at = Column(DateTime) ``` ChannelDeviceMapping 表 ```python class ChannelDeviceMapping(Base): __tablename__ = "channel_device_mappings" id = Column(String, primary_key=True) channel_id = Column(String, ForeignKey("audio_channels.id")) device_id = Column(String) # 操作系统设备标识符 ``` ProfileChannelMapping 表 ```python class ProfileChannelMapping(Base): __tablename__ = "profile_channel_mappings" profile_id = Column(String, ForeignKey("profiles.id"), primary_key=True) channel_id = Column(String, ForeignKey("audio_channels.id"), primary_key=True) ``` 默认通道 数据库初始化时会创建一个默认通道: ```python def init_db(): # 如果默认通道不存在则创建 default_channel = db.query(AudioChannel).filter( AudioChannel.is_default == True ).first() if not default_channel: default_channel = AudioChannel( id=str(uuid.uuid4()), name="Default", is_default=True ) db.add(default_channel) # 将所有现有配置文件分配给默认通道 profiles = db.query(VoiceProfile).all() for profile in profiles: mapping = ProfileChannelMapping( profile_id=profile.id, channel_id=default_channel.id ) db.add(mapping) ``` 核心操作 创建通道 ```python async def create_channel( data: AudioChannelCreate, db: Session, ) -> AudioChannelResponse: # 检查名称唯一性 existing = db.query(DBAudioChannel).filter_by(name=data.name).first() if existing: raise ValueError(f"名称为 '{data.name}' 的通道已存在") # 创建通道 channel = DBAudioChannel( id=str(uuid.uuid4()), name=data.name, is_default=False, ) db.add(channel) # 添加设备映射 for device_id in data.device_ids: mapping = DBChannelDeviceMapping( id=str(uuid.uuid4()), channel_id=channel.id, device_id=device_id, ) db.add(mapping) db.commit() ``` 更新通道 ```python async def update_channel( channel_id: str, data: AudioChannelUpdate, db: Session, ) -> AudioChannelResponse: channel = db.query(DBAudioChannel).filter_by(id=channel_id).first() # 不能修改默认通道 if channel.is_default: raise ValueError("无法修改默认通道") # 更新名称 if data.name is not None: channel.name = data.name # 更新设备映射 if data.device_ids is not None: # 删除现有映射 db.query(DBChannelDeviceMapping).filter_by(channel_id=channel_id).delete() # 添加新映射 for device_id in data.device_ids: mapping = DBChannelDeviceMapping( channel_id=channel.id, device_id=device_id, ) db.add(mapping) db.commit() ``` 删除通道 ```python async def delete_channel(channel_id: str, db: Session) -> bool: channel = db.query(DBAudioChannel).filter_by(id=channel_id).first() # 不能删除默认通道 if channel.is_default: raise ValueError("无法删除默认通道") # 删除设备映射 db.query(DBChannelDeviceMapping).filter_by(channel_id=channel_id).delete() # 删除配置文件-通道映射 db.query(DBProfileChannelMapping).filter_by(channel_id=channel_id).delete() # 删除通道 db.delete(channel) db.commit() ``` 语音分配 为通道分配语音 ```python async def set_channel_voices( channel_id: str, data: ChannelVoiceAssignment, db: Session, ) -> None: # 验证通道是否存在 channel = db.query(DBAudioChannel).filter_by(id=channel_id).first() if not channel: raise ValueError(f"未找到通道 {channel_id}") # 验证所有配置文件是否存在 for profile_id in data.profile_ids: profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first() if not profile: raise ValueError(f"未找到配置文件 {profile_id}") # 删除现有映射 db.query(DBProfileChannelMapping).filter_by(channel_id=channel_id).delete() # 添加新映射 for profile_id in data.profile_ids: mapping = DBProfileChannelMapping( profile_id=profile_id, channel_id=channel_id, ) db.add(mapping) db.commit() ``` 为语音分配通道 ```python async def set_profile_channels( profile_id: str, data: ProfileChannelAssignment, db: Session, ) -> None: # 验证配置文件是否存在 profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first() if not profile: raise ValueError(f"未找到配置文件 {profile_id}") # 删除现有映射 db.query(DBProfileChannelMapping).filter_by(profile_id=profile_id).delete() # 添加新映射 for channel_id in data.channel_ids: mapping = DBProfileChannelMapping( profile_id=profile_id, channel_id=channel_id, ) db.add(mapping) db.commit() ``` API 端点 | 方法 | 端点 | 描述 | | :--- | :--- | :--- | | GET | /channels | 列出所有通道 | | POST | /channels | 创建通道 | | GET | /channels/{id} | 根据 ID 获取通道 | | PUT | /channels/{id} | 更新通道 | | DELETE | /channels/{id} | 删除通道 | | GET | /channels/{id}/voices | 获取已分配的语音 | | PUT | /channels/{id}/voices | 设置已分配的语音 | | GET | /profiles/{id}/channels | 获取配置文件的通道 | | PUT | /profiles/{id}/channels | 设置配置文件的通道 |
请求/响应模式 AudioChannelCreate ```json { "name": "Speakers", "device_ids": ["device_uuid_1", "device_uuid_2"] } ``` AudioChannelResponse ```json { "id": "channel_uuid", "name": "Speakers", "is_default": false, "device_ids": ["device_uuid_1", "device_uuid_2"], "created_at": "2024-01-15T10:30:00Z" } ``` ChannelVoiceAssignment ```json { "profile_ids": ["profile_1", "profile_2"] } ``` 使用场景 多输出设置 场景:使用不同的语音角色进行直播 * 创建 "Stream" 通道 → 指向 OBS 虚拟音频 * 创建 "Monitor" 通道 → 指向耳机 * 将 "Narrator" 配置文件分配给 → 两个通道 * 将 "Character 1" 配置文件分配给 → 仅限 Stream 通道
虚拟音频线缆 虚拟音频的常用设备 ID: * VB-Audio Virtual Cable * BlackHole (macOS) * Soundflower (macOS)
前端集成 前端需要执行以下操作: * 使用 Web Audio API 或 Tauri 枚举设备 * 显示带有设备分配信息的通道列表 * 允许通过拖拽或下拉菜单进行配置文件分配 * 根据配置文件的通道将播放路由到正确的设备
局限性 * 设备 ID 是操作系统相关的 * 热插拔可能导致设备 ID 失效 * 默认通道无法被修改或删除 * 实际音频路由由前端处理(后端仅存储配置)
在 GitHub 上编辑 转录:Whisper 音频转录在 Voicebox 中的工作原理 模型管理:模型下载、加载及状态跟踪在所有引擎中的工作原理