خط أنابيب الصوت لسطح المكتب
توفر حزمة adk-audio إدخال/إخراج الصوت عبر سطح المكتب على مختلف المنصات خلف علامة الميزات desktop-audio. وتتيح ثلاثة مكونات — AudioCapture وAudioPlayback وVadTurnManager — التقاط الصوت من الميكروفون، وتشغيل الصوت عبر مكبرات الصوت، وتناوب الأدوار المعتمد على VAD لبناء وكلاء صوتيين لسطح المكتب.
نظرة عامة
يربط خط أنابيب الصوت لسطح المكتب أجهزة الصوت في النظام بنظام خطوط الأنابيب adk-audio الحالي:
Microphone → AudioCapture → AudioStream → [VAD → STT → Agent → TTS] → AudioPlayback → Speaker
تستخدم جميع المكونات حزمة cpal للصوت عبر مختلف المنصات (CoreAudio على macOS، وPipeWire/ALSA/PulseAudio على Linux، وWASAPI على Windows)، وتنتج/تستهلك النوع القياسي 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 على PipeWire بشفافية عبر طبقة التوافق مع ALSA، من دون الحاجة إلى إعداد إضافي.
من المخطط إضافة واجهة خلفية أصلية لـ PipeWire (desktop-pipewire) تستخدم حزمة pipewire (v0.9) في إصدار مستقبلي. سيوفر PipeWire الأصلي زمن استجابة أقل وإدارة مباشرة للجلسات. وهو محجوب حاليًا بسبب تعارض في التبعيات: إذ تثبّت redis-protocol 6.0.0 الإصدار cookie-factory =0.3.2، بينما تتطلب libspa الخاصة بـpipewire الإصدار 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_rate | u32 | 16000 | معدل أخذ العينات بالهرتز |
channels | u8 | 1 | عدد القنوات (1=أحادي، 2=استريو) |
frame_duration_ms | u32 | 20 | مدة كل AudioFrame |
استدعِ validate() قبل الاستخدام — يرفض القيم الصفرية باستخدام AudioError::Device.
AudioCapture
التقاط الصوت من الميكروفون عبر cpal. تُرجع طريقة start_capture() قيمة AudioStream (وهي mpsc::Receiver<AudioFrame> محدودة السعة بسعة 64). تُنتَج الإطارات بفواصل زمنية قدرها frame_duration_ms وبتنسيق PCM-16 LE.
AudioPlayback
تشغيل الصوت عبر cpal. تُضيف طريقة play() عينات AudioFrame إلى مخزن مؤقت مشترك يفرغه ردّ اتصال إخراج cpal. استدعِ stop() لتحرير الجهاز.
VadTurnManager
يستهلك AudioStream، ويطبّق VadProcessor::is_speech() على كل إطار، ويصدر قيم VoiceActivityEvent عبر ردّ اتصال مسجّل.
وضعان:
- HandsFree — اكتشاف تلقائي لحدود الكلام باستخدام عتبات قابلة للتهيئة لمدة الصمت والكلام
- PushToTalk — لا توجد أحداث تلقائية؛ ويتحكم المستدعي في البوابة خارجيًا
VadConfig
| الحقل | النوع | الوصف |
|---|---|---|
mode | VadMode | HandsFree أو PushToTalk |
silence_threshold_ms | u32 | الصمت المتواصل قبل SpeechEnded |
speech_threshold_ms | u32 | الكلام المتتالي قبل SpeechStarted |
استدعِ validate() قبل الاستخدام — إذ يرفض العتبات الصفرية باستخدام AudioError::Vad.
بناء وكيل صوتي
النمط الكامل لوكيل صوتي تحاوري:
- التقاط الصوت من الميكروفون
- اكتشاف حدود الكلام باستخدام VAD
- تحويل الكلام إلى نص باستخدام GeminiStt (أو أي
SttProvider) - إرسال النص إلى LlmAgent للاستدلال
- توليف الاستجابة باستخدام GeminiTts (أو أي
TtsProvider) - تشغيل الصوت المُولَّف عبر مكبر الصوت
راجع examples/desktop_audio/src/voice_agent.rs للحصول على مثال عملي كامل يستخدم موفّري Gemini السحابيين الفعليين.
أمان الخيوط
جميع أنواع الصوت على سطح المكتب (AudioCapture وAudioPlayback وVadTurnManager) هي Send + Sync، مما يجعل مشاركتها آمنة عبر مهام Tokio.
معالجة الأخطاء
تستخدم أخطاء الصوت على سطح المكتب تعداد AudioError الموجود:
| المكوّن | صيغة الخطأ | متى |
|---|---|---|
| AudioCapture | AudioError::Device | الجهاز غير موجود، المضيف غير متاح، التحقق من صحة الإعدادات |
| AudioPlayback | AudioError::Device | الجهاز غير موجود، المضيف غير متاح، فشل الفتح/الكتابة |
| VadTurnManager | AudioError::Vad | التحقق من الإعدادات (الحدود الدنيا الصفرية) |
دعم المنصة
| المنصة | الواجهة الخلفية للصوت | الحالة |
|---|---|---|
| macOS | CoreAudio | مدعوم |
| Linux | PipeWire (عبر توافق ALSA) | مدعوم (ثبّت libasound2-dev) |
| Linux | ALSA / PulseAudio (قديم) | مدعوم (ثبّت libasound2-dev) |
| Windows | WASAPI | مدعوم |
في أنظمة Linux الحديثة، يُعدّ PipeWire خادم الصوت الافتراضي، وقد حلّ محلّ PulseAudio وALSA. تعمل ميزة desktop-audio على PipeWire بشفافية من خلال طبقة التوافق مع ALSA الخاصة بها. ومن المخطط توفير خلفية PipeWire أصلية تستخدم crate pipewire في إصدار مستقبلي (وهي محجوبة حاليًا بسبب تعارض في التبعيات upstream داخل redis-protocol).
أمثلة
راجع examples/desktop_audio/ للاطلاع على 6 أمثلة عملية، بما في ذلك وكيل صوتي حواري متكامل باستخدام Gemini STT/TTS الحقيقي.