桌面音频管道

adk-audio crate 在 desktop-audio 功能标志后提供跨平台桌面音频 I/O。三个组件——AudioCaptureAudioPlaybackVadTurnManager——支持麦克风采集、扬声器播放以及由 VAD 驱动的轮流发言,用于构建桌面语音代理。

概览

桌面音频管道将系统音频硬件连接到现有的 adk-audio 管道系统:

Microphone → AudioCapture → AudioStream → [VAD → STT → Agent → TTS] → AudioPlayback → Speaker

所有组件都使用 cpal crate 实现跨平台音频(macOS 上使用 CoreAudio,Linux 上使用 PipeWire/ALSA/PulseAudio,Windows 上使用 WASAPI),并生成/使用标准的 AudioFrame 类型。

功能标志

[dependencies]
# Cross-platform (macOS, Linux, Windows) via cpal
adk-audio = { version = "2.1.0", features = ["desktop-audio"] }

desktop-audio 功能包含 vad(用于 VadProcessor),并添加 cpal 作为依赖项。它有意排除在 all 功能之外,以避免在 CI 构建中引入特定平台的音频依赖。

PipeWire 支持

在现代 Linux(Fedora 34+、Ubuntu 22.10+、Arch)上,PipeWire 已取代 PulseAudio 和 ALSA,成为默认音频服务器。desktop-audio 功能通过其 ALSA 兼容层透明地运行于 PipeWire 之上——无需额外配置。

未来版本计划提供原生 PipeWire 后端(desktop-pipewire),使用 pipewire crate(v0.9)。原生 PipeWire 将提供更低的延迟和直接的会话管理。目前,该功能受依赖冲突阻碍:redis-protocol 6.0.0 固定使用 cookie-factory =0.3.2,而 pipewirelibspa 要求使用 0.3.3。一旦上游放宽此固定版本限制,即可添加原生后端。

快速开始

列出音频设备

use adk_audio::{AudioCapture, AudioPlayback};

let inputs = AudioCapture::list_input_devices()?;
for device in &inputs {
    println!("Mic: {} ({})", device.name(), device.id());
}

let outputs = AudioPlayback::list_output_devices()?;
for device in &outputs {
    println!("Speaker: {} ({})", device.name(), device.id());
}

采集麦克风音频

use adk_audio::{AudioCapture, CaptureConfig};
use std::time::{Duration, Instant};

let mut capture = AudioCapture::new();
let devices = AudioCapture::list_input_devices()?;
let device = devices.first().expect("no input device");

let config = CaptureConfig::default(); // 16kHz, mono, 20ms frames
let mut stream = capture.start_capture(device.id(), &config)?;

let start = Instant::now();
while start.elapsed() < Duration::from_secs(3) {
    if let Some(frame) = stream.recv().await {
        // frame.data: PCM-16 LE bytes
        // frame.sample_rate: 16000
        // frame.channels: 1
        // frame.duration_ms: 20
    }
}
capture.stop_capture();

通过扬声器播放音频

use adk_audio::{AudioFrame, AudioPlayback};

let mut playback = AudioPlayback::new();
let devices = AudioPlayback::list_output_devices()?;
let device = devices.first().expect("no output device");

let frame = AudioFrame::silence(16000, 1, 1000); // 1 second
playback.play(device.id(), &frame).await?;
playback.stop();

VAD 轮流发言

use std::sync::Arc;
use adk_audio::{
    AudioCapture, CaptureConfig, VadConfig, VadMode,
    VadTurnManager, VoiceActivityEvent, VadProcessor,
};

let vad: Arc<dyn VadProcessor> = /* your VadProcessor impl */;
let config = VadConfig {
    mode: VadMode::HandsFree,
    silence_threshold_ms: 500,
    speech_threshold_ms: 200,
};

let mut manager = VadTurnManager::new(vad, config)?;
let mut capture = AudioCapture::new();
let stream = capture.start_capture(device_id, &CaptureConfig::default())?;

manager.start(stream, |event| {
    match event {
        VoiceActivityEvent::SpeechStarted => println!("Speech started"),
        VoiceActivityEvent::SpeechEnded { duration_ms } => {
            println!("Speech ended ({duration_ms}ms)");
        }
    }
});

组件

AudioDevice

系统音频设备(输入或输出)的描述符。包含不透明的 id 和人类可读的 name

CaptureConfig

麦克风采集的配置:

字段类型默认值描述
sample_rateu3216000采样率(单位:Hz)
channelsu81声道数(1=单声道,2=立体声)
frame_duration_msu3220每个 AudioFrame 的持续时间

使用前调用 validate() — 使用 AudioError::Device 拒绝零值。

AudioCapture

通过 cpal 采集麦克风音频。start_capture() 方法返回一个 AudioStream(容量为 64 的有界 mpsc::Receiver<AudioFrame>)。帧以 frame_duration_ms 间隔生成,格式为 PCM-16 LE。

AudioPlayback

通过 cpal 播放扬声器音频。play() 方法将 AudioFrame 的采样写入共享缓冲区,该缓冲区由 cpal 输出回调排空。调用 stop() 以释放设备。

VadTurnManager

消费一个 AudioStream,对每个帧应用 VadProcessor::is_speech(),并通过已注册的回调发出 VoiceActivityEvent 值。

两种模式:

  • HandsFree — 使用可配置的静音时长和语音时长阈值自动检测语音边界
  • PushToTalk — 不生成自动事件;由调用方在外部控制门控

VadConfig

字段类型描述
modeVadModeHandsFreePushToTalk
silence_threshold_msu32SpeechEnded 之前的连续静默
speech_threshold_msu32SpeechStarted 之前的连续语音

使用前调用 validate() — 使用 AudioError::Vad 拒绝零阈值。

构建语音代理

完整的对话式语音代理模式:

  1. 从麦克风捕获音频
  2. 使用 VAD 检测语音边界
  3. 使用 GeminiStt(或任何 SttProvider)转录语音
  4. 将转录文本发送给 LlmAgent 进行推理
  5. 使用 GeminiTts(或任何 TtsProvider)合成响应
  6. 通过扬声器播放合成的音频

请参阅 examples/desktop_audio/src/voice_agent.rs,了解使用真实 Gemini 云提供商的完整可运行示例。

线程安全

所有桌面音频类型(AudioCaptureAudioPlaybackVadTurnManager)均为 Send + Sync,因此可以安全地在 Tokio 任务之间共享。

错误处理

桌面音频错误使用现有的 AudioError 枚举:

组件错误变体适用情况
AudioCaptureAudioError::Device未找到设备、主机不可用、配置验证
AudioPlaybackAudioError::Device未找到设备、主机不可用、打开/写入失败
VadTurnManagerAudioError::Vad配置验证(零阈值)

平台支持

平台音频后端状态
macOSCoreAudio支持
LinuxPipeWire(通过 ALSA 兼容层)支持(安装 libasound2-dev
LinuxALSA / PulseAudio(旧版)支持(安装 libasound2-dev
WindowsWASAPI支持

在现代 Linux 上,PipeWire 是默认的音频服务器,已取代 PulseAudio 和 ALSA。desktop-audio 功能通过其 ALSA 兼容层透明地运行于 PipeWire 之上。计划在未来版本中提供使用 pipewire crate 的原生 PipeWire 后端(目前因 redis-protocol 中的上游依赖冲突而受阻)。

示例

请参阅 examples/desktop_audio/,其中包含 6 个实用示例,包括一个使用真实 Gemini STT/TTS 的完整对话式语音代理。