aic_sdk/model.rs
1use crate::error::*;
2
3use aic_sdk_sys::*;
4
5use std::{
6 ffi::{CStr, CString},
7 marker::PhantomData,
8 path::Path,
9 ptr,
10};
11
12/// High-level wrapper for an ai-coustics model.
13///
14/// A single model instance can be used to create multiple processors, VADs or analyzers,
15/// according to the model type.
16///
17/// Each processor, VAD or analyzer created with a given model keeps the underlying model
18/// alive through internal reference counting. When the reference count reaches zero the
19/// model is destroyed. You may therefore drop the model before those objects, in any order.
20///
21/// # Sharing and Multi-threading
22///
23/// `Model` is `Send` and `Sync`, so you can share it across threads. It does not implement
24/// `Clone`, so wrap it in an `Arc` if you need shared ownership.
25///
26/// # Example
27///
28/// ```rust,no_run
29/// # use aic_sdk::{Model, ProcessorConfig, Processor};
30/// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
31/// let model = Model::from_file("/path/to/model.aicmodel")?;
32/// let config = ProcessorConfig::optimal(&model);
33/// let mut processor = Processor::new(&model, &license_key)?;
34/// processor.initialize(&config)?;
35/// let mut audio_block = vec![0.0f32; config.block_size];
36/// processor.process(&mut audio_block)?;
37/// # Ok::<(), aic_sdk::AicError>(())
38/// ```
39///
40/// # Multi-threaded Example
41///
42/// ```rust,no_run
43/// # use aic_sdk::{Model, ProcessorConfig, Processor};
44/// # use std::{thread, sync::Arc};
45/// let model = Arc::new(Model::from_file("/path/to/model.aicmodel")?);
46///
47/// // Spawn multiple threads, each with its own processor but sharing the same model
48/// let handles: Vec<_> = (0..4)
49/// .map(|i| {
50/// let model_clone = Arc::clone(&model);
51/// thread::spawn(move || {
52/// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
53/// let mut processor = Processor::new(&model_clone, &license_key).unwrap();
54/// // Process audio in this thread...
55/// })
56/// })
57/// .collect();
58///
59/// for handle in handles {
60/// handle.join().unwrap();
61/// }
62/// # Ok::<(), aic_sdk::AicError>(())
63/// ```
64pub struct Model<'a> {
65 /// Raw pointer to the C model structure
66 ptr: *mut AicModel,
67 /// Marker to tie the lifetime of the model to the lifetime of its weights
68 marker: PhantomData<&'a [u8]>,
69}
70
71impl<'a> Model<'a> {
72 /// Creates a new model instance from a model file.
73 ///
74 /// A single model instance can be used to create multiple processors, VADs or analyzers,
75 /// according to the model type.
76 ///
77 /// # Lifetime and ownership
78 ///
79 /// Each processor, VAD or analyzer created with a given model keeps the underlying model
80 /// alive through internal reference counting. When the reference count reaches zero the
81 /// model is destroyed. You may therefore drop the model before those objects, in any order.
82 ///
83 /// The model data is memory-mapped from the file, not copied into the process. Make sure
84 /// the file is not modified or deleted while the model, or any object created from it, is
85 /// alive.
86 ///
87 /// # Arguments
88 ///
89 /// * `path` - Filesystem path to a model file.
90 ///
91 /// # Returns
92 ///
93 /// Returns a `Result` containing the new `Model` instance or an `AicError` if creation fails.
94 ///
95 /// # Example
96 ///
97 /// ```rust,no_run
98 /// # use aic_sdk::Model;
99 /// let model = Model::from_file("/path/to/model.aicmodel")?;
100 /// # Ok::<(), aic_sdk::AicError>(())
101 /// ```
102 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Model<'static>, AicError> {
103 let mut model_ptr: *mut AicModel = ptr::null_mut();
104 let c_path = CString::new(path.as_ref().to_string_lossy().as_bytes()).unwrap();
105
106 // SAFETY:
107 // - `model_ptr` points to stack memory we own.
108 // - `c_path` is a valid, null-terminated string.
109 // - This function is not thread-safe, but the output pointer and path
110 // buffer are local to this call and not shared with other threads.
111 let error_code = unsafe { aic_model_create_from_file(&mut model_ptr, c_path.as_ptr()) };
112
113 handle_error(error_code)?;
114
115 // This should never happen if the C library is well-behaved, but let's be defensive
116 assert!(
117 !model_ptr.is_null(),
118 "C library returned success but null pointer"
119 );
120
121 Ok(Model {
122 ptr: model_ptr,
123 marker: PhantomData,
124 })
125 }
126
127 /// Creates a new model instance from a memory buffer.
128 ///
129 /// A single model instance can be used to create multiple processors, VADs or analyzers,
130 /// according to the model type.
131 ///
132 /// # Lifetime and ownership
133 ///
134 /// Each processor, VAD or analyzer created with a given model keeps the underlying model
135 /// alive through internal reference counting. When the reference count reaches zero the
136 /// model is destroyed. You may therefore drop the model before those objects, in any order.
137 ///
138 /// The buffer must be 64-byte aligned.
139 ///
140 /// Consider using [`include_model!`](macro@crate::include_model) to embed a model file at compile time with
141 /// the correct alignment.
142 ///
143 /// # Arguments
144 ///
145 /// * `buffer` - Raw bytes of the model file.
146 ///
147 /// # Returns
148 ///
149 /// Returns a `Result` containing the new `Model` instance or an `AicError` if creation fails.
150 ///
151 /// # Example
152 ///
153 /// ```rust,no_run
154 /// # use aic_sdk::{include_model, Model};
155 /// static MODEL: &[u8] = include_model!("path/to/model.aicmodel");
156 /// let model = Model::from_buffer(MODEL)?;
157 /// # Ok::<(), aic_sdk::AicError>(())
158 /// ```
159 pub fn from_buffer(buffer: &'a [u8]) -> Result<Self, AicError> {
160 let mut model_ptr: *mut AicModel = ptr::null_mut();
161
162 // SAFETY:
163 // - `buffer` is a valid slice and immutable for `'a`.
164 // - The SDK only reads from `buffer` for the lifetime of the model.
165 // - This function is not thread-safe, but the output pointer is local to
166 // this call and no model handle exists until it returns.
167 let error_code =
168 unsafe { aic_model_create_from_buffer(&mut model_ptr, buffer.as_ptr(), buffer.len()) };
169
170 handle_error(error_code)?;
171
172 // This should never happen if the C library is well-behaved, but let's be defensive
173 assert!(
174 !model_ptr.is_null(),
175 "C library returned success but null pointer"
176 );
177
178 Ok(Model {
179 ptr: model_ptr,
180 marker: PhantomData,
181 })
182 }
183
184 /// Returns the model identifier.
185 ///
186 /// The returned string is UTF-8 encoded.
187 pub fn id(&self) -> &str {
188 // SAFETY:
189 // - `self` owns a valid model pointer created by the SDK.
190 // - The returned pointer is only used while `self` keeps the model alive.
191 // - This function is not thread-safe with concurrent destruction, which
192 // Rust prevents while `&self` is live.
193 let id_ptr = unsafe { aic_model_get_id(self.as_const_ptr()) };
194 if id_ptr.is_null() {
195 return "unknown";
196 }
197
198 // SAFETY: Pointer is valid for the lifetime of `self` and is null-terminated.
199 unsafe { CStr::from_ptr(id_ptr).to_str().unwrap_or("unknown") }
200 }
201
202 /// Retrieves the optimal sample rate of the model.
203 ///
204 /// Each model is optimized for a specific sample rate, which determines the frequency
205 /// range of the enhanced audio output. While you can process audio at any sample rate,
206 /// understanding the model's native rate helps predict the enhancement quality.
207 ///
208 /// **How sample rate affects enhancement:**
209 /// - Models trained at lower sample rates (e.g., 8 kHz) can only enhance frequencies
210 /// up to their Nyquist limit (4 kHz for 8 kHz models)
211 /// - When processing higher sample rate input (e.g., 48 kHz) with a lower-rate model,
212 /// only the lower frequency components will be enhanced
213 ///
214 /// **Enhancement blending:**
215 /// When enhancement strength is set below 1.0, the enhanced signal is blended with
216 /// the original, maintaining the full frequency spectrum of your input while adding
217 /// the model's noise reduction capabilities to the lower frequencies.
218 ///
219 /// **Sample rate and optimal block size relationship:**
220 /// When using different sample rates than the model's native rate, the optimal samples
221 /// per block (returned by [`Model::optimal_block_size`]) will change. The processor's output delay remains
222 /// constant regardless of sample rate as long as you use the optimal block size for
223 /// that rate.
224 ///
225 /// **Recommendation:**
226 /// For maximum enhancement quality across the full frequency spectrum, match your
227 /// input sample rate to the model's native rate when possible.
228 ///
229 /// # Returns
230 ///
231 /// Returns the model's native sample rate.
232 ///
233 /// # Example
234 ///
235 /// ```rust,no_run
236 /// # use aic_sdk::{Model, Processor};
237 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
238 /// # let model = Model::from_file("/path/to/model.aicmodel")?;
239 /// let optimal_sample_rate = model.optimal_sample_rate();
240 /// println!("Optimal sample rate: {optimal_sample_rate} Hz");
241 /// # Ok::<(), aic_sdk::AicError>(())
242 /// ```
243 pub fn optimal_sample_rate(&self) -> u32 {
244 let mut sample_rate: u32 = 0;
245 // SAFETY:
246 // - `self.as_const_ptr()` is a valid pointer to a live model.
247 // - `sample_rate` points to stack storage for output.
248 // - This function can be called from any thread, so we only borrow `&self`.
249 let error_code =
250 unsafe { aic_model_get_optimal_sample_rate(self.as_const_ptr(), &mut sample_rate) };
251
252 // This should never fail. If it does, it's a bug in the SDK.
253 // `aic_model_get_optimal_sample_rate` is documented to always succeed if given valid pointers.
254 assert_success(
255 error_code,
256 "`aic_model_get_optimal_sample_rate` failed. This is a bug, please open an issue on GitHub for further investigation.",
257 );
258
259 // This should never fail
260 sample_rate
261 }
262
263 /// Retrieves the optimal block size for the model at a given sample rate.
264 ///
265 /// Using the optimal block size minimizes latency by avoiding internal buffering.
266 ///
267 /// **When you use a different block size than the optimal value, the processor will
268 /// introduce additional buffering latency on top of its base processing delay.**
269 ///
270 /// The optimal block size varies based on the sample rate. Each model operates on a
271 /// fixed time window length, so the required number of samples changes with sample rate.
272 /// For example, a model designed for 10 ms processing windows requires 480 samples at
273 /// 48 kHz, but only 160 samples at 16 kHz to capture the same duration of audio.
274 ///
275 /// Call this function with your intended sample rate before calling
276 /// [`Processor::initialize`](crate::Processor::initialize) to determine the best block size for minimal latency.
277 ///
278 /// # Arguments
279 ///
280 /// * `sample_rate` - The sample rate in Hz for which to calculate the optimal block size.
281 ///
282 /// # Returns
283 ///
284 /// Returns the optimal block size.
285 ///
286 /// # Example
287 ///
288 /// ```rust,no_run
289 /// # use aic_sdk::{Model, Processor};
290 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
291 /// # let model = Model::from_file("/path/to/model.aicmodel")?;
292 /// # let sample_rate = model.optimal_sample_rate();
293 /// let optimal_block_size = model.optimal_block_size(sample_rate);
294 /// println!("Optimal block size: {optimal_block_size}");
295 /// # Ok::<(), aic_sdk::AicError>(())
296 /// ```
297 pub fn optimal_block_size(&self, sample_rate: u32) -> usize {
298 let mut block_size: usize = 0;
299 // SAFETY:
300 // - `self.as_const_ptr()` is a valid pointer to a live model.
301 // - `block_size` points to stack storage for output.
302 // - This function can be called from any thread, so we only borrow `&self`.
303 let error_code = unsafe {
304 aic_model_get_optimal_block_size(self.as_const_ptr(), sample_rate, &mut block_size)
305 };
306
307 // This should never fail. If it does, it's a bug in the SDK.
308 // `aic_model_get_optimal_block_size` is documented to always succeed if given valid pointers.
309 assert_success(
310 error_code,
311 "`aic_model_get_optimal_block_size` failed. This is a bug, please open an issue on GitHub for further investigation.",
312 );
313
314 block_size
315 }
316
317 /// Downloads a model file from the ai-coustics artifact CDN.
318 ///
319 /// This method verifies that the requested model exists in a version compatible
320 /// with this library, and downloads the model file to the specified directory. If
321 /// the model file already exists, it will not be re-downloaded. If the existing
322 /// file's checksum does not match, the model will be downloaded and the existing
323 /// file will be replaced.
324 ///
325 /// The artifact manifest is cached in `download_dir` and shared by all models.
326 /// It is refetched when it expires, or when a cached entry turns out to be
327 /// stale.
328 ///
329 /// Available models can be browsed at [artifacts.ai-coustics.io](https://artifacts.ai-coustics.io/).
330 ///
331 /// # Arguments
332 ///
333 /// * `model_id` - The model identifier (e.g., `"quail-l-16khz"`).
334 /// * `download_dir` - Directory where the model file will be stored.
335 ///
336 /// # Returns
337 ///
338 /// Returns the full path to the model file on success, or an [`AicError`] if the
339 /// operation fails.
340 ///
341 /// # Note
342 ///
343 /// This is a blocking operation that performs network I/O.
344 #[cfg(feature = "download-model")]
345 pub fn download<P: AsRef<Path>>(
346 model_id: &str,
347 download_dir: P,
348 ) -> Result<std::path::PathBuf, AicError> {
349 let compatible_version = crate::get_compatible_model_version();
350 aic_model_downloader::download(model_id, compatible_version, download_dir)
351 .map_err(|err| AicError::ModelDownload(err.to_string()))
352 }
353
354 pub(crate) fn as_const_ptr(&self) -> *const AicModel {
355 self.ptr as *const AicModel
356 }
357}
358
359impl<'a> Drop for Model<'a> {
360 fn drop(&mut self) {
361 if !self.ptr.is_null() {
362 // SAFETY:
363 // - `self.ptr` was allocated by the SDK and is still owned by this wrapper.
364 // - This function is not thread-safe with concurrent model use, but
365 // `drop` has exclusive access to `self`.
366 unsafe { aic_model_destroy(self.ptr) };
367 }
368 }
369}
370
371// SAFETY:
372// - Model wraps a raw pointer to an AicModel which is immutable after creation and it
373// does not provide access to it through its public API.
374// - Methods only pass the pointer to SDK calls documented as thread-safe for const access.
375unsafe impl<'a> Send for Model<'a> {}
376// SAFETY:
377// - Model wraps a raw pointer to an AicModel which is immutable after creation and it
378// does not provide access to it through its public API.
379// - Methods only pass the pointer to SDK calls documented as thread-safe for const access.
380unsafe impl<'a> Sync for Model<'a> {}
381
382/// Embeds the bytes of model file, ensuring proper alignment.
383///
384/// This macro uses Rust's standard library's [`include_bytes!`](std::include_bytes) macro
385/// to include the model file at compile time.
386///
387/// # Example
388///
389/// ```rust,no_run
390/// # use aic_sdk::{include_model, Model};
391///
392/// static MODEL: &[u8] = include_model!("path/to/model.aicmodel");
393/// let model = Model::from_buffer(MODEL)?;
394/// # Ok::<(), aic_sdk::AicError>(())
395/// ```
396#[macro_export]
397macro_rules! include_model {
398 ($path:expr) => {{
399 #[repr(C, align(64))]
400 struct __Aligned<T: ?Sized>(T);
401
402 const __DATA: &'static __Aligned<[u8; include_bytes!($path).len()]> =
403 &__Aligned(*include_bytes!($path));
404
405 &__DATA.0
406 }};
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn include_model_aligns_to_64_bytes() {
415 // Use the README.md as a dummy file for testing
416 let data = include_model!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"));
417
418 let ptr = data.as_ptr() as usize;
419 assert!(
420 ptr.is_multiple_of(64),
421 "include_model should align data to 64 bytes"
422 );
423 }
424
425 #[test]
426 fn model_is_send_and_sync() {
427 // Compile-time check that Model implements Send and Sync.
428 // This ensures the model can be safely shared across threads.
429 fn assert_send<T: Send>() {}
430 fn assert_sync<T: Sync>() {}
431
432 assert_send::<Model>();
433 assert_sync::<Model>();
434 }
435}
436
437#[doc(hidden)]
438mod _compile_fail_tests {
439 //! Compile-fail regression: a `Model` created from a buffer must not outlive the buffer.
440 //!
441 //! ```rust,compile_fail
442 //! use aic_sdk::Model;
443 //!
444 //! fn leak_model_from_buffer() -> Model<'static> {
445 //! let bytes = vec![0u8; 64];
446 //! let model = Model::from_buffer(&bytes).unwrap();
447 //! model
448 //! }
449 //!
450 //! fn main() {
451 //! let _ = leak_model_from_buffer();
452 //! }
453 //! ```
454}