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}