Skip to main content

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