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