Skip to main content

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}