aic_sdk/analyzer.rs
1use crate::{error::*, model::Model, processor::ProcessorConfig};
2
3use aic_sdk_sys::*;
4
5use std::{ffi::CString, marker::PhantomData, ptr};
6
7/// The result of analyzing an audio signal with an [`Analyzer`].
8///
9/// Scores are in the range `0.0..=1.0`. For all fields except
10/// [`speaker_loudness`](Self::speaker_loudness), lower values indicate less problematic audio.
11#[derive(Debug, Clone, PartialEq)]
12pub struct AnalysisResult {
13 /// Headline audio score.
14 ///
15 /// Predicts likelihood of failure of downstream models including speech-to-text,
16 /// voice activity detection or turn-taking or speech-to-speech models.
17 /// Lower indicates less problematic audio.
18 ///
19 /// **Range:** 0.0 to 1.0
20 pub risk_score: f32,
21 /// Measure of speaker distance and reverberance.
22 /// Lower indicates less problematic audio.
23 ///
24 /// **Range:** 0.0 to 1.0
25 pub speaker_reverb: f32,
26 /// Measure of speaker loudness.
27 ///
28 /// **Range:** 0.0 to 1.0
29 pub speaker_loudness: f32,
30 /// Measure of interfering speech from sources other than the main speaker.
31 /// Lower indicates less problematic audio.
32 ///
33 /// **Range:** 0.0 to 1.0
34 pub interfering_speech: f32,
35 /// Measure of ambient or environmental noise.
36 /// Lower indicates less problematic audio.
37 ///
38 /// **Range:** 0.0 to 1.0
39 pub noise: f32,
40 /// Measure of artifacts introduced by lossy speech codecs,
41 /// e.g. from a low bitrate or a narrowband codec.
42 /// Lower indicates less problematic audio.
43 ///
44 /// **Range:** 0.0 to 1.0
45 pub codec_degradation: f32,
46 /// Measure of audio dropouts or discontinuities in the stream,
47 /// e.g. from packet loss, frame erasure, jitter or CPU overload.
48 /// Lower indicates less problematic audio.
49 ///
50 /// **Range:** 0.0 to 1.0
51 pub packet_loss: f32,
52}
53
54impl From<AicAnalysisResult> for AnalysisResult {
55 fn from(value: AicAnalysisResult) -> Self {
56 Self {
57 risk_score: value.risk_score,
58 speaker_reverb: value.speaker_reverb,
59 speaker_loudness: value.speaker_loudness,
60 interfering_speech: value.interfering_speech,
61 noise: value.noise,
62 codec_degradation: value.codec_degradation,
63 packet_loss: value.packet_loss,
64 }
65 }
66}
67
68/// Creates a collector/analyzer pair for non-real-time analysis.
69///
70/// The collector is designed to be placed in the audio thread, buffering audio chunks for
71/// later analysis.
72///
73/// The analyzer is designed to be run separately. Analysis models are computationally expensive
74/// and cannot run in the audio thread. The analyzer has access to the audio buffered by the
75/// collector, and it can access it safely across threads.
76///
77/// The collector retains a span of audio determined by the analysis model. As more samples
78/// get collected, old audio is discarded.
79///
80/// # Arguments
81///
82/// * `model` - The loaded model instance. Must be an analysis model, otherwise
83/// [`AicError::ModelTypeUnsupported`] is returned.
84/// * `license_key` - license key for the ai-coustics SDK
85/// (generate your key at [developers.ai-coustics.com](https://developers.ai-coustics.com/))
86///
87/// # Warning
88///
89/// This function allocates memory. Do not call it from audio processing threads.
90///
91/// # Example
92///
93/// ```rust,no_run
94/// # use aic_sdk::Model;
95/// let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
96/// let model = Model::from_file("/path/to/model.aicmodel")?;
97/// let (mut collector, mut analyzer) = aic_sdk::analyzer_pair(&model, &license_key)?;
98/// # Ok::<(), aic_sdk::AicError>(())
99/// ```
100pub fn analyzer_pair<'a>(
101 model: &Model<'a>,
102 license_key: &str,
103) -> Result<(Collector, Analyzer<'a>), AicError> {
104 // Set the wrapper ID as soon as the user attempts to instantiate an analyzer.
105 // SAFETY: `2` is the wrapper ID assigned to this Rust SDK.
106 unsafe { crate::set_sdk_id(2) };
107
108 let mut collector_ptr: *mut AicCollector = ptr::null_mut();
109 let mut analyzer_ptr: *mut AicAnalyzer = ptr::null_mut();
110 let c_license_key = CString::new(license_key).map_err(|_| AicError::LicenseFormatInvalid)?;
111
112 // SAFETY:
113 // - `collector_ptr` and `analyzer_ptr` point to stack storage for output.
114 // - `model` is a valid SDK model pointer for the duration of the call.
115 // - `c_license_key` is a null-terminated CString.
116 // - This function is not thread-safe, but the output pointers are local to
117 // this call and neither handle exists until it returns.
118 let error_code = unsafe {
119 aic_analyzer_pair_create(
120 &mut collector_ptr,
121 &mut analyzer_ptr,
122 model.as_const_ptr(),
123 c_license_key.as_ptr(),
124 )
125 };
126
127 handle_error(error_code)?;
128
129 assert!(
130 !collector_ptr.is_null(),
131 "C library returned success but null collector pointer"
132 );
133 assert!(
134 !analyzer_ptr.is_null(),
135 "C library returned success but null analyzer pointer"
136 );
137
138 let collector = Collector::new(collector_ptr);
139 let analyzer = Analyzer::new(analyzer_ptr, model);
140
141 Ok((collector, analyzer))
142}
143
144/// Buffers audio for later analysis.
145///
146/// The collector is designed to be placed in the audio thread,
147/// buffering audio chunks for the [`Analyzer`] to analyze later.
148pub struct Collector {
149 /// Raw pointer to the C collector structure.
150 inner: *mut AicCollector,
151 /// Whether `initialize` has been called.
152 initialized: bool,
153}
154
155impl Collector {
156 fn new(collector_ptr: *mut AicCollector) -> Self {
157 Self {
158 inner: collector_ptr,
159 initialized: false,
160 }
161 }
162
163 /// Configures the collector for a specific audio format.
164 ///
165 /// This function must be called before buffering any audio.
166 /// Using the sample rate and block size returned by [`Model::optimal_sample_rate`] and
167 /// [`Model::optimal_block_size`] avoids internal resampling and rebuffering.
168 ///
169 /// # Arguments
170 ///
171 /// * `config` - Audio buffering configuration
172 ///
173 /// # Returns
174 ///
175 /// Returns `Ok(())` on success or an `AicError` if initialization fails.
176 ///
177 /// # Warning
178 /// Do not call from audio processing threads as this allocates memory.
179 ///
180 /// # Example
181 ///
182 /// ```rust,no_run
183 /// # use aic_sdk::{Model, ProcessorConfig};
184 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
185 /// # let model = Model::from_file("/path/to/model.aicmodel")?;
186 /// # let (mut collector, _) = aic_sdk::analyzer_pair(&model, &license_key)?;
187 /// let config = ProcessorConfig::optimal(&model);
188 /// collector.initialize(&config)?;
189 /// # Ok::<(), aic_sdk::AicError>(())
190 /// ```
191 pub fn initialize(&mut self, config: &ProcessorConfig) -> Result<(), AicError> {
192 // SAFETY:
193 // - `self.inner` is a valid pointer to a live collector.
194 // - This function is not thread-safe, so we borrow `&mut self`.
195 let error_code = unsafe {
196 aic_collector_initialize(
197 self.inner,
198 config.sample_rate,
199 config.block_size,
200 config.variable_block_size,
201 )
202 };
203
204 handle_error(error_code)?;
205 self.initialized = true;
206 Ok(())
207 }
208
209 /// Buffers audio for later offline use.
210 ///
211 /// # Arguments
212 ///
213 /// * `audio` - Mono audio block to be buffered. Must match `block_size` from
214 /// initialization, or if `variable_block_size` was enabled, must be less than or equal
215 /// to `block_size`.
216 ///
217 /// # Returns
218 ///
219 /// Returns `Ok(())` on success or an [`AicError`] if buffering fails.
220 ///
221 /// # Real-time safety
222 ///
223 /// Real-time safe. Can be called from audio processing threads.
224 ///
225 /// # Example
226 ///
227 /// ```rust,no_run
228 /// # use aic_sdk::{Model, ProcessorConfig};
229 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
230 /// # let model = Model::from_file("/path/to/model.aicmodel")?;
231 /// # let (mut collector, _) = aic_sdk::analyzer_pair(&model, &license_key)?;
232 /// let config = ProcessorConfig::optimal(&model);
233 /// collector.initialize(&config)?;
234 /// let audio = vec![0.0f32; config.block_size];
235 /// collector.buffer(&audio)?;
236 /// # Ok::<(), aic_sdk::AicError>(())
237 /// ```
238 pub fn buffer(&mut self, audio: &[f32]) -> Result<(), AicError> {
239 if !self.initialized {
240 return Err(AicError::NotInitialized);
241 }
242
243 let audio_len = audio.len();
244
245 // SAFETY:
246 // - `self.inner` is a valid pointer to a live collector.
247 // - `audio` points to a contiguous, f32 slice of length `audio_len`.
248 // - This function is not thread-safe, so we borrow `&mut self`.
249 let error_code = unsafe { aic_collector_buffer(self.inner, audio.as_ptr(), audio_len) };
250
251 handle_error(error_code)
252 }
253}
254
255impl Drop for Collector {
256 fn drop(&mut self) {
257 if !self.inner.is_null() {
258 // SAFETY:
259 // - `self.inner` was allocated by the SDK and is still owned by this wrapper.
260 // - This function is not thread-safe with concurrent collector use,
261 // but `drop` has exclusive access to `self`.
262 unsafe { aic_collector_destroy(self.inner) };
263 }
264 }
265}
266
267// SAFETY: Everything in Collector is Send, with the exception of the inner raw pointer.
268// The Collector only uses the raw pointer according to the safety contracts of the
269// unsafe APIs that require the pointer, and the Collector does not expose access to the
270// raw pointer in any of its methods. Therefore, it safe to implement Send for Collector.
271unsafe impl Send for Collector {}
272
273// SAFETY: Collector does not expose any interior mutability, and all unsafe APIs that make use of
274// the inner raw pointer uphold the thread safety contracts required by the unsafe APIs.
275// Therefore, it is safe to implement Sync for Collector.
276unsafe impl Sync for Collector {}
277
278/// Runs an analysis model over the audio buffered by a [`Collector`].
279///
280/// The analyzer is designed to be run in a non-audio thread. Analysis models are computationally expensive
281/// and cannot run in the audio thread. The analyzer has access to the audio buffered by the
282/// collector, and it can access it safely across threads.
283pub struct Analyzer<'a> {
284 /// Raw pointer to the C analyzer structure.
285 inner: *mut AicAnalyzer,
286 /// Marker to tie the analyzer to the lifetime of the model's weights.
287 marker: PhantomData<&'a [u8]>,
288}
289
290impl<'a> Analyzer<'a> {
291 fn new(analyzer_ptr: *mut AicAnalyzer, _model: &Model<'a>) -> Self {
292 Self {
293 inner: analyzer_ptr,
294 marker: PhantomData,
295 }
296 }
297
298 fn as_const_ptr(&self) -> *const AicAnalyzer {
299 self.inner as *const AicAnalyzer
300 }
301
302 /// Clears all internal state and buffers.
303 ///
304 /// Call this when the audio stream is interrupted or when seeking
305 /// to prevent mispredictions from previous audio content.
306 ///
307 /// This operates on both the analyzer and its collector.
308 ///
309 /// The [`Collector`] stays initialized to the configured settings.
310 ///
311 /// # Returns
312 ///
313 /// Returns `Ok(())` on success or an [`AicError`] if the reset fails.
314 ///
315 /// # Real-time safety
316 ///
317 /// Real-time safe. Can be called from audio processing threads.
318 ///
319 /// # Example
320 ///
321 /// ```rust,no_run
322 /// # use aic_sdk::Model;
323 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
324 /// # let model = Model::from_file("/path/to/model.aicmodel")?;
325 /// # let (_, mut analyzer) = aic_sdk::analyzer_pair(&model, &license_key)?;
326 /// analyzer.reset()?;
327 /// # Ok::<(), aic_sdk::AicError>(())
328 /// ```
329 pub fn reset(&self) -> Result<(), AicError> {
330 // SAFETY:
331 // - `self.as_const_ptr()` is a valid pointer to a live analyzer.
332 // - This function can be called from any thread, so we only borrow `&self`.
333 let error_code = unsafe { aic_analyzer_reset(self.as_const_ptr()) };
334 handle_error(error_code)
335 }
336
337 /// Analyze the buffered signal.
338 ///
339 /// The analyzer runs a forward-pass of the analysis model with a fixed length of audio,
340 /// determined by the model.
341 ///
342 /// If this function is called before the collector has buffered that length of audio,
343 /// the analyzer will run the analysis with silence (zeros) in the tail of the input.
344 ///
345 /// # Returns
346 ///
347 /// Returns an [`AnalysisResult`] if successful, otherwise an [`AicError`].
348 ///
349 /// # Real-time safety
350 ///
351 /// This function is not real-time safe. Avoid calling it from audio threads.
352 pub fn analyze_buffered(&mut self) -> Result<AnalysisResult, AicError> {
353 let mut result = AicAnalysisResult {
354 risk_score: 0.0,
355 speaker_reverb: 0.0,
356 speaker_loudness: 0.0,
357 interfering_speech: 0.0,
358 noise: 0.0,
359 codec_degradation: 0.0,
360 packet_loss: 0.0,
361 };
362
363 // SAFETY:
364 // - `self.inner` is a valid pointer to a live analyzer.
365 // - `result` points to stack storage for output.
366 // - This function is not thread-safe, so we borrow `&mut self`.
367 let error_code = unsafe { aic_analyzer_analyze_buffered(self.inner, &mut result) };
368 handle_error(error_code)?;
369
370 Ok(result.into())
371 }
372
373 /// Terminates the telemetry session associated with this analyzer.
374 ///
375 /// Once the request has been handled, the analyzer is no longer allowed to analyze
376 /// buffered audio.
377 ///
378 /// This function is meant to be used in lifecycle management events.
379 /// A telemetry session is automatically stopped when an analyzer is destroyed.
380 /// However, in cases where this SDK is integrated with languages with automatic memory
381 /// management, object deallocation could be delayed. Use this function to terminate
382 /// the session explicitly.
383 ///
384 /// This function blocks until the telemetry session is terminated, unless another
385 /// session is still alive. In that case, this function returns early and termination
386 /// happens asynchronously. This keeps lifecycle management smooth while ensuring
387 /// all sessions are closed when the last telemetry session is terminated.
388 ///
389 /// # Returns
390 ///
391 /// Returns `Ok(())` on success or an [`AicError`] if termination cannot be requested.
392 ///
393 /// # Real-time safety
394 ///
395 /// This function is not real-time safe. It may block until the session is terminated.
396 /// Avoid calling it from audio threads.
397 ///
398 /// # Example
399 ///
400 /// ```rust,no_run
401 /// # use aic_sdk::Model;
402 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
403 /// # let model = Model::from_file("/path/to/model.aicmodel")?;
404 /// # let (_, mut analyzer) = aic_sdk::analyzer_pair(&model, &license_key)?;
405 /// analyzer.terminate_session()?;
406 /// # Ok::<(), aic_sdk::AicError>(())
407 /// ```
408 pub fn terminate_session(&mut self) -> Result<(), AicError> {
409 // SAFETY:
410 // - `self.inner` is a valid pointer to a live analyzer.
411 // - This function must not run concurrently with any other call taking the same
412 // analyzer handle, so we borrow `&mut self`.
413 let error_code = unsafe { aic_analyzer_terminate_session(self.inner) };
414 handle_error(error_code)
415 }
416
417 /// Replaces the bearer token on a running analyzer.
418 ///
419 /// Use this when your license key is a JWT and needs to be refreshed
420 /// before it expires. Calling this with a renewed token lets you stay authenticated
421 /// without tearing down and recreating the analyzer: the analyzer handle stays valid,
422 /// buffered audio remains available, and the new token is used for all
423 /// subsequent authentication against the ai-coustics backend.
424 ///
425 /// In-place updates are only supported when both the originally configured key and the
426 /// new token are JWTs. Other license types cannot be swapped in this way.
427 ///
428 /// On any error the call is a no-op: the previously active token stays in use and the
429 /// telemetry session is unaffected (no backoff, no interruption to processing).
430 ///
431 /// On success the swap is applied immediately and is **not** gated on backend
432 /// acceptance. The token is validated locally for format only; if the backend later
433 /// rejects it (e.g. expired or revoked), the SDK retries it under backoff rather than
434 /// rolling back to the prior token, and analysis calls may be rejected if no
435 /// accepted token arrives in time. Supplying a known-good token via this call
436 /// during that window recovers the session.
437 ///
438 /// # Arguments
439 ///
440 /// * `token` - The new JWT to install.
441 ///
442 /// # Returns
443 ///
444 /// Returns `Ok(())` on success or an `AicError` if the update fails.
445 ///
446 /// # Real-time safety
447 ///
448 /// This function is not real-time safe. It locks a mutex and allocates memory.
449 /// Avoid calling it from audio threads.
450 ///
451 /// # Example
452 ///
453 /// ```rust,no_run
454 /// # use aic_sdk::Model;
455 /// # let license_key = std::env::var("AIC_SDK_LICENSE").unwrap();
456 /// # let model = Model::from_file("/path/to/model.aicmodel")?;
457 /// # let (_, analyzer) = aic_sdk::analyzer_pair(&model, &license_key)?;
458 /// let renewed_jwt = String::from("<JWT_BEARER_TOKEN>");
459 /// analyzer.update_bearer_token(&renewed_jwt)?;
460 /// # Ok::<(), aic_sdk::AicError>(())
461 /// ```
462 pub fn update_bearer_token(&self, token: &str) -> Result<(), AicError> {
463 let c_token = CString::new(token).map_err(|_| AicError::LicenseFormatInvalid)?;
464
465 // SAFETY:
466 // - `self.as_const_ptr()` is a valid pointer to a live analyzer.
467 // - `c_token` is a null-terminated CString that outlives the call.
468 // - This function can run concurrently with collector buffering; Rust
469 // prevents concurrent analyze or destroy on the same analyzer handle.
470 let error_code =
471 unsafe { aic_analyzer_update_bearer_token(self.as_const_ptr(), c_token.as_ptr()) };
472 handle_error(error_code)
473 }
474}
475
476impl<'a> Drop for Analyzer<'a> {
477 fn drop(&mut self) {
478 if !self.inner.is_null() {
479 // SAFETY:
480 // - `self.inner` was allocated by the SDK and is still owned by this wrapper.
481 // - This function is not thread-safe with concurrent analyzer use,
482 // but `drop` has exclusive access to `self`.
483 unsafe { aic_analyzer_destroy(self.inner) };
484 }
485 }
486}
487
488// SAFETY: Everything in Analyzer is Send, with the exception of the inner raw pointer.
489// The Analyzer only uses the raw pointer according to the safety contracts of the
490// unsafe APIs that require the pointer, and the Analyzer does not expose access to the
491// raw pointer in any of its methods. Therefore, it safe to implement Send for Analyzer.
492unsafe impl<'a> Send for Analyzer<'a> {}
493
494// SAFETY: Analyzer does not expose any interior mutability, and all unsafe APIs that make use of
495// the inner raw pointer uphold the thread safety contracts required by the unsafe APIs.
496// Therefore, it is safe to implement Sync for Analyzer.
497unsafe impl<'a> Sync for Analyzer<'a> {}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502 use crate::test_support::{license_key, test_model_path};
503
504 /// The only analysis model this SDK version can load.
505 const TEST_MODEL_ID: &str = "tyto-1.1-l-16khz";
506
507 fn load_test_model() -> Result<(Model<'static>, String), AicError> {
508 let model = Model::from_file(test_model_path(TEST_MODEL_ID))?;
509
510 Ok((model, license_key()))
511 }
512
513 fn test_analyzer_pair(
514 model: &Model<'static>,
515 license_key: &str,
516 ) -> (Collector, Analyzer<'static>) {
517 analyzer_pair(model, license_key)
518 .expect("tyto-1.1-l-16khz should create a collector/analyzer pair")
519 }
520
521 fn assert_score_range(result: &AnalysisResult) {
522 assert!((0.0..=1.0).contains(&result.risk_score));
523 assert!((0.0..=1.0).contains(&result.speaker_reverb));
524 assert!((0.0..=1.0).contains(&result.speaker_loudness));
525 assert!((0.0..=1.0).contains(&result.interfering_speech));
526 assert!((0.0..=1.0).contains(&result.noise));
527 assert!((0.0..=1.0).contains(&result.codec_degradation));
528 assert!((0.0..=1.0).contains(&result.packet_loss));
529 }
530
531 #[test]
532 fn analysis_result_maps_all_ffi_fields() {
533 let ffi_result = AicAnalysisResult {
534 risk_score: 0.1,
535 speaker_reverb: 0.2,
536 speaker_loudness: 0.3,
537 interfering_speech: 0.4,
538 noise: 0.5,
539 codec_degradation: 0.6,
540 packet_loss: 0.7,
541 };
542
543 assert_eq!(
544 AnalysisResult::from(ffi_result),
545 AnalysisResult {
546 risk_score: 0.1,
547 speaker_reverb: 0.2,
548 speaker_loudness: 0.3,
549 interfering_speech: 0.4,
550 noise: 0.5,
551 codec_degradation: 0.6,
552 packet_loss: 0.7,
553 }
554 );
555 }
556
557 #[test]
558 fn collector_rejects_buffering_before_initialize() {
559 let mut collector = Collector {
560 inner: ptr::null_mut(),
561 initialized: false,
562 };
563
564 let audio = vec![0.0f32; 4];
565
566 assert_eq!(collector.buffer(&audio), Err(AicError::NotInitialized));
567 }
568
569 #[test]
570 fn analyzer_pair_rejects_license_key_with_nul() {
571 let (model, _) = load_test_model().unwrap();
572
573 let result = analyzer_pair(&model, "invalid\0license");
574
575 assert!(matches!(result, Err(AicError::LicenseFormatInvalid)));
576 }
577
578 #[test]
579 fn collector_buffers_audio_and_analyzer_returns_scores() {
580 let (model, license_key) = load_test_model().unwrap();
581 let (mut collector, mut analyzer) = test_analyzer_pair(&model, &license_key);
582 let config = ProcessorConfig::optimal(&model);
583 collector.initialize(&config).unwrap();
584
585 let audio = vec![0.0f32; config.block_size];
586 collector.buffer(&audio).unwrap();
587
588 let result = analyzer.analyze_buffered().unwrap();
589 assert_score_range(&result);
590 }
591
592 #[test]
593 fn collector_buffers_variable_block_size_when_enabled() {
594 let (model, license_key) = load_test_model().unwrap();
595 let (mut collector, _analyzer) = test_analyzer_pair(&model, &license_key);
596 let config = ProcessorConfig::optimal(&model).with_variable_block_size(true);
597 collector.initialize(&config).unwrap();
598
599 let full = vec![0.0f32; config.block_size];
600 collector.buffer(&full).unwrap();
601
602 let short = vec![0.0f32; 20];
603 collector.buffer(&short).unwrap();
604 }
605
606 #[test]
607 fn collector_rejects_variable_block_size_when_disabled() {
608 let (model, license_key) = load_test_model().unwrap();
609 let (mut collector, _analyzer) = test_analyzer_pair(&model, &license_key);
610 let config = ProcessorConfig::optimal(&model);
611 collector.initialize(&config).unwrap();
612
613 let full = vec![0.0f32; config.block_size];
614 collector.buffer(&full).unwrap();
615
616 let short = vec![0.0f32; 20];
617 assert_eq!(collector.buffer(&short), Err(AicError::AudioConfigMismatch));
618 }
619
620 #[test]
621 fn analyzer_reset_keeps_collector_initialized() {
622 let (model, license_key) = load_test_model().unwrap();
623 let (mut collector, mut analyzer) = test_analyzer_pair(&model, &license_key);
624 let config = ProcessorConfig::optimal(&model);
625 collector.initialize(&config).unwrap();
626
627 analyzer.reset().unwrap();
628
629 let audio = vec![0.0f32; config.block_size];
630 collector.buffer(&audio).unwrap();
631
632 let result = analyzer.analyze_buffered().unwrap();
633 assert_score_range(&result);
634 }
635
636 #[test]
637 fn model_can_be_dropped_after_creating_analyzer_pair() {
638 let (model, license_key) = load_test_model().unwrap();
639 let config = ProcessorConfig::optimal(&model);
640 let (mut collector, mut analyzer) = test_analyzer_pair(&model, &license_key);
641 drop(model); // The SDK keeps the model data alive for analyzer instances created from files.
642
643 collector.initialize(&config).unwrap();
644
645 let audio = vec![0.0f32; config.block_size];
646 collector.buffer(&audio).unwrap();
647
648 let result = analyzer.analyze_buffered().unwrap();
649 assert_score_range(&result);
650 }
651
652 #[test]
653 fn collector_and_analyzer_are_send_and_sync() {
654 // Compile-time check that Collector and Analyzer can cross thread boundaries.
655 fn assert_send<T: Send>() {}
656 fn assert_sync<T: Send>() {}
657
658 assert_send::<Collector>();
659 assert_sync::<Collector>();
660 assert_send::<Analyzer>();
661 assert_sync::<Analyzer>();
662 }
663}
664
665#[doc(hidden)]
666mod _compile_fail_tests {
667 //! Compile-fail regression: an `Analyzer`'s model buffer must not be dropped before the analyzer.
668 //!
669 //! ```rust,compile_fail
670 //! use aic_sdk::{Model, ProcessorConfig, analyzer_pair};
671 //!
672 //! fn main() {
673 //! let buffer = vec![0u8; 64];
674 //! let model = Model::from_buffer(&buffer).unwrap();
675 //! let config = ProcessorConfig::optimal(&model);
676 //!
677 //! let (mut collector, mut analyzer) = analyzer_pair(&model, "license").unwrap();
678 //! collector.initialize(&config).unwrap();
679 //!
680 //! drop(model); // Model can be dropped without issues
681 //!
682 //! drop(buffer); // This should fail to compile
683 //!
684 //! let audio = vec![0.0f32; config.block_size];
685 //! collector.buffer(&audio).unwrap();
686 //! analyzer.analyze_buffered().unwrap();
687 //! }
688 //! ```
689}