aic_sdk/vad_async.rs
1use crate::{
2 AicError, Model, OtelConfig, ProcessorConfig, Vad, VadContext,
3 processor_async::get_global_thread_pool,
4};
5use async_lock::Mutex;
6use futures_channel::oneshot;
7use std::sync::Arc;
8
9/// A wrapper around [`Vad`] for use in async contexts.
10///
11/// # Threading
12///
13/// Processing runs on the same background thread pool as
14/// [`ProcessorAsync`](crate::ProcessorAsync), shared across all instances. The pool defaults to
15/// one thread per logical CPU. Override with the `AIC_NUM_THREADS` environment variable, which is
16/// read once on first use.
17///
18/// # Example
19///
20/// ```rust,no_run
21/// use aic_sdk::{Model, ProcessorConfig, VadAsync};
22/// #[tokio::main]
23/// async fn main() -> Result<(), aic_sdk::AicError> {
24/// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
25/// let model = Model::from_file("/path/to/vad_model.aicmodel")?;
26/// let config = ProcessorConfig::optimal(&model);
27///
28/// let vad = VadAsync::new(&model, &license_key)?.with_config(&config).await?;
29/// let vad_ctx = vad.context().await;
30///
31/// let mut audio = vec![0.0f32; config.block_size];
32/// for _ in 0..2 {
33/// // `process` hands the block back, so the same allocation can be reused.
34/// audio = vad.process(audio).await?;
35/// println!("Speech detected: {}", vad_ctx.is_speech_detected());
36/// }
37/// Ok(())
38/// }
39/// ```
40pub struct VadAsync {
41 inner: Arc<Mutex<Vad<'static>>>,
42}
43
44impl VadAsync {
45 /// Creates a new async voice activity detector instance.
46 ///
47 /// See [`Vad::new`] for details.
48 pub fn new(model: &Model<'static>, license_key: &str) -> Result<Self, AicError> {
49 let vad = Vad::new(model, license_key)?;
50 Ok(Self {
51 inner: Arc::new(Mutex::new(vad)),
52 })
53 }
54
55 /// Creates a new async voice activity detector instance with explicit
56 /// OpenTelemetry configuration.
57 ///
58 /// See [`Vad::with_otel_config`] for details.
59 pub fn with_otel_config(
60 model: &Model<'static>,
61 license_key: &str,
62 otel_config: &OtelConfig,
63 ) -> Result<Self, AicError> {
64 let vad = Vad::with_otel_config(model, license_key, otel_config)?;
65 Ok(Self {
66 inner: Arc::new(Mutex::new(vad)),
67 })
68 }
69
70 /// Initializes the async VAD with the given configuration.
71 ///
72 /// This is a convenience method that calls [`VadAsync::initialize`]
73 /// internally and returns `self`.
74 pub async fn with_config(self, config: &ProcessorConfig) -> Result<Self, AicError> {
75 self.initialize(config).await?;
76 Ok(self)
77 }
78
79 /// Initializes the VAD with the given configuration.
80 ///
81 /// See [`Vad::initialize`] for details.
82 ///
83 /// # Warning
84 /// This allocates memory internally. Do not call from latency-sensitive paths.
85 pub async fn initialize(&self, config: &ProcessorConfig) -> Result<(), AicError> {
86 let config = config.clone();
87 let (tx, rx) = oneshot::channel();
88 let mut vad = self.inner.lock_arc().await;
89 get_global_thread_pool().spawn(move || {
90 let _ = tx.send(vad.initialize(&config));
91 });
92 rx.await.expect("Rayon worker dropped")
93 }
94
95 /// Processes mono audio and updates the VAD prediction.
96 ///
97 /// This method takes ownership of `audio`, moves it to a background processing
98 /// thread, and returns the audio block unmodified. Ownership is required because the
99 /// background thread outlives the borrow if this future is cancelled; handing the block
100 /// back lets a streaming loop reuse the same allocation for every block.
101 ///
102 /// See [`Vad::process`] for details.
103 pub async fn process(&self, audio: Vec<f32>) -> Result<Vec<f32>, AicError> {
104 let (tx, rx) = oneshot::channel();
105 let mut vad = self.inner.lock_arc().await;
106 get_global_thread_pool().spawn(move || {
107 let result = vad.process(&audio).map(|_| audio);
108 let _ = tx.send(result);
109 });
110 rx.await.expect("Rayon worker dropped")
111 }
112
113 /// Terminates the telemetry session associated with this VAD.
114 ///
115 /// See [`Vad::terminate_session`] for details.
116 ///
117 /// # Warning
118 /// This may block until the session is terminated, so it runs on the background
119 /// thread pool rather than the calling task.
120 pub async fn terminate_session(&self) -> Result<(), AicError> {
121 let (tx, rx) = oneshot::channel();
122 let mut vad = self.inner.lock_arc().await;
123 get_global_thread_pool().spawn(move || {
124 let _ = tx.send(vad.terminate_session());
125 });
126 rx.await.expect("Rayon worker dropped")
127 }
128
129 /// Returns a [`VadContext`] to read the prediction and control parameters.
130 ///
131 /// See [`Vad::context`] for details.
132 pub async fn context(&self) -> VadContext {
133 self.inner.lock().await.context()
134 }
135}