Skip to main content

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}