Skip to main content

aic_sdk/
processor_async.rs

1use crate::{
2    AicError, Model, OtelConfig, Processor, ProcessorConfig, ProcessorContext, VadContext,
3};
4use async_lock::Mutex;
5use futures_channel::oneshot;
6use std::sync::{Arc, OnceLock};
7
8static RAYON_POOL: OnceLock<rayon::ThreadPool> = OnceLock::new();
9
10fn get_global_thread_pool() -> &'static rayon::ThreadPool {
11    RAYON_POOL.get_or_init(|| {
12        let num_threads = std::env::var("AIC_NUM_THREADS")
13            .ok()
14            .and_then(|s| s.parse::<usize>().ok())
15            .filter(|&n| n > 0)
16            .unwrap_or_else(|| {
17                std::thread::available_parallelism()
18                    .map(|n| n.get())
19                    .unwrap_or(1)
20            });
21
22        rayon::ThreadPoolBuilder::new()
23            .num_threads(num_threads)
24            .thread_name(|i| format!("aic-processing-thread-{i}"))
25            .build()
26            .expect("failed to build aic thread pool")
27    })
28}
29
30/// A wrapper around [`Processor`] for use in async contexts.
31///
32/// # Threading
33///
34/// Processing runs on a background thread pool shared across all
35/// [`ProcessorAsync`] instances. The pool defaults to one thread per logical
36/// CPU. Override with the `AIC_NUM_THREADS` environment variable, which is
37/// read once on first use.
38///
39/// # Example
40///
41/// ```rust,no_run
42/// use aic_sdk::{Model, ProcessorAsync, ProcessorConfig};
43/// #[tokio::main]
44/// async fn main() -> Result<(), aic_sdk::AicError> {
45///     let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
46///     let model = Model::from_file("/path/to/model.aicmodel")?;
47///     let config = ProcessorConfig::optimal(&model).with_num_channels(2);
48///
49///     let processor = ProcessorAsync::new(&model, &license_key)?.with_config(&config).await?;
50///
51///     let mut audio = vec![0.0f32; config.num_channels as usize * config.num_frames];
52///     let audio = processor.process_interleaved(audio).await?;
53///     Ok(())
54/// }
55/// ```
56pub struct ProcessorAsync {
57    inner: Arc<Mutex<Processor<'static>>>,
58}
59
60impl ProcessorAsync {
61    /// Creates a new async audio enhancement processor instance.
62    ///
63    /// See [`Processor::new`] for details.
64    pub fn new(model: &Model<'static>, license_key: &str) -> Result<Self, AicError> {
65        let processor = Processor::new(model, license_key)?;
66        Ok(Self {
67            inner: Arc::new(Mutex::new(processor)),
68        })
69    }
70
71    /// Creates a new async audio enhancement processor instance with explicit
72    /// OpenTelemetry configuration.
73    ///
74    /// See [`Processor::with_otel_config`] for details.
75    pub fn with_otel_config(
76        model: &Model<'static>,
77        license_key: &str,
78        otel_config: &OtelConfig,
79    ) -> Result<Self, AicError> {
80        let processor = Processor::with_otel_config(model, license_key, otel_config)?;
81        Ok(Self {
82            inner: Arc::new(Mutex::new(processor)),
83        })
84    }
85
86    /// Initializes the async processor with the given configuration.
87    ///
88    /// This is a convenience method that calls [`ProcessorAsync::initialize`]
89    /// internally and returns `self`.
90    pub async fn with_config(self, config: &ProcessorConfig) -> Result<Self, AicError> {
91        self.initialize(config).await?;
92        Ok(self)
93    }
94
95    /// Initializes the processor with the given configuration.
96    ///
97    /// See [`Processor::initialize`] for details.
98    ///
99    /// # Warning
100    /// This allocates memory internally. Do not call from latency-sensitive paths.
101    pub async fn initialize(&self, config: &ProcessorConfig) -> Result<(), AicError> {
102        let config = config.clone();
103        let (tx, rx) = oneshot::channel();
104        let mut processor = self.inner.lock_arc().await;
105        get_global_thread_pool().spawn(move || {
106            let _ = tx.send(processor.initialize(&config));
107        });
108        rx.await.expect("Rayon worker dropped")
109    }
110
111    /// Processes audio with interleaved channel data.
112    ///
113    /// This method takes ownership of `audio`, moves it to a background processing
114    /// thread, and returns the processed buffer.
115    ///
116    /// See [`Processor::process_interleaved`] for details on the memory layout.
117    pub async fn process_interleaved(&self, mut audio: Vec<f32>) -> Result<Vec<f32>, AicError> {
118        let (tx, rx) = oneshot::channel();
119        let mut processor = self.inner.lock_arc().await;
120        get_global_thread_pool().spawn(move || {
121            let result = processor.process_interleaved(&mut audio).map(|_| audio);
122            let _ = tx.send(result);
123        });
124        rx.await.expect("Rayon worker dropped")
125    }
126
127    /// Processes audio with separate buffers for each channel (planar layout).
128    ///
129    /// This method takes ownership of `audio`, moves it to a background processing
130    /// thread, and returns the processed channel buffers.
131    ///
132    /// See [`Processor::process_planar`] for details on the memory layout.
133    pub async fn process_planar(
134        &self,
135        mut audio: Vec<Vec<f32>>,
136    ) -> Result<Vec<Vec<f32>>, AicError> {
137        let (tx, rx) = oneshot::channel();
138        let mut processor = self.inner.lock_arc().await;
139        get_global_thread_pool().spawn(move || {
140            let result = processor.process_planar(&mut audio).map(|_| audio);
141            let _ = tx.send(result);
142        });
143        rx.await.expect("Rayon worker dropped")
144    }
145
146    /// Processes audio with sequential channel data.
147    ///
148    /// This method takes ownership of `audio`, moves it to a background processing
149    /// thread, and returns the processed buffer.
150    ///
151    /// See [`Processor::process_sequential`] for details on the memory layout.
152    pub async fn process_sequential(&self, mut audio: Vec<f32>) -> Result<Vec<f32>, AicError> {
153        let (tx, rx) = oneshot::channel();
154        let mut processor = self.inner.lock_arc().await;
155        get_global_thread_pool().spawn(move || {
156            let result = processor.process_sequential(&mut audio).map(|_| audio);
157            let _ = tx.send(result);
158        });
159        rx.await.expect("Rayon worker dropped")
160    }
161
162    /// Returns a [`ProcessorContext`] for real-time parameter control.
163    ///
164    /// See [`Processor::processor_context`] for details.
165    pub async fn processor_context(&self) -> ProcessorContext {
166        self.inner.lock().await.processor_context()
167    }
168
169    /// Returns a [`VadContext`] for voice activity detection.
170    ///
171    /// See [`Processor::vad_context`] for details.
172    pub async fn vad_context(&self) -> VadContext {
173        self.inner.lock().await.vad_context()
174    }
175}