Skip to main content

voirs_ffi/
lib.rs

1//! # VoiRS FFI (Foreign Function Interface)
2//!
3//! C-compatible bindings for VoiRS speech synthesis framework with comprehensive
4//! language support for C/C++, Python, Node.js, and WebAssembly.
5//!
6//! ## Features
7//!
8//! - **C API**: Complete C-compatible interface for maximum portability
9//! - **Python Bindings**: PyO3-based Python integration with NumPy support
10//! - **Node.js Bindings**: N-API bindings for JavaScript/TypeScript
11//! - **WebAssembly**: WASM bindings for browser-based synthesis
12//! - **Zero-Copy Operations**: Efficient memory management across FFI boundaries
13//! - **Thread Safety**: Comprehensive threading support with work-stealing schedulers
14//! - **Platform Integration**: Native integration with Windows, macOS, and Linux
15//!
16//! ## Module Organization
17//!
18//! - [`c_api`]: Core C API functions for synthesis, audio, and voice management
19//! - [`python`]: PyO3 bindings for Python integration (requires `python` feature)
20//! - [`nodejs`]: N-API bindings for Node.js (requires `nodejs` feature)
21//! - [`wasm`]: WebAssembly bindings (requires `wasm` feature)
22//! - [`memory`]: Advanced memory management with custom allocators and zero-copy operations
23//! - [`threading`]: Thread pools, synchronization primitives, and callback management
24//! - [`error`]: Comprehensive error handling with i18n support
25//! - [`performance`]: Performance monitoring and optimization utilities
26//! - [`platform`]: Platform-specific integrations (Windows, macOS, Linux)
27//! - [`utils`]: Utility functions for audio processing, string conversion, and performance analysis
28//!
29//! ## Quick Start (C API)
30//!
31//! ```c
32//! #include "voirs_ffi.h"
33//!
34//! // Initialize pipeline
35//! VoirsPipelineHandle* pipeline = voirs_create_pipeline();
36//!
37//! // Synthesize speech
38//! VoirsAudioBuffer* buffer = NULL;
39//! VoirsErrorCode result = voirs_synthesize(
40//!     pipeline,
41//!     "Hello, world!",
42//!     &buffer
43//! );
44//!
45//! if (result == VOIRS_SUCCESS) {
46//!     // Process audio...
47//!     voirs_free_audio_buffer(buffer);
48//! }
49//!
50//! voirs_destroy_pipeline(pipeline);
51//! ```
52//!
53//! ## Quick Start (Python)
54//!
55//! ```python
56//! from voirs_ffi import VoirsPipeline
57//!
58//! # Create pipeline
59//! pipeline = VoirsPipeline()
60//!
61//! # Synthesize speech
62//! result = pipeline.synthesize("Hello, world!")
63//! audio_data = result.audio_data  # NumPy array
64//! ```
65//!
66//! ## Safety
67//!
68//! All FFI functions are marked as `unsafe` and require careful handling:
69//! - Null pointer checks for all pointer parameters
70//! - Proper memory management (use provided free functions)
71//! - Thread safety guarantees where documented
72//! - No undefined behavior when contracts are followed
73//!
74//! ## Performance
75//!
76//! The FFI layer is designed for minimal overhead:
77//! - Zero-copy operations where possible
78//! - Efficient memory pooling
79//! - SIMD-optimized audio processing
80//! - Work-stealing thread pools for parallelism
81
82// Allow pedantic lints that are acceptable for audio/DSP processing code
83#![allow(clippy::cast_precision_loss)] // Acceptable for audio sample conversions
84#![allow(clippy::cast_possible_truncation)] // Controlled truncation in audio processing
85#![allow(clippy::cast_sign_loss)] // Intentional in index calculations
86#![allow(clippy::missing_errors_doc)] // Many internal functions with self-documenting error types
87#![allow(clippy::missing_panics_doc)] // Panics are documented where relevant
88#![allow(clippy::unused_self)] // Some trait implementations require &self for consistency
89#![allow(clippy::must_use_candidate)] // Not all return values need must_use annotation
90#![allow(clippy::doc_markdown)] // Technical terms don't all need backticks
91#![allow(clippy::unnecessary_wraps)] // Result wrappers maintained for API consistency
92#![allow(clippy::float_cmp)] // Exact float comparisons are intentional in some contexts
93#![allow(clippy::match_same_arms)] // Pattern matching clarity sometimes requires duplication
94#![allow(clippy::module_name_repetitions)] // Type names often repeat module names
95#![allow(clippy::struct_excessive_bools)] // Config structs naturally have many boolean flags
96#![allow(clippy::too_many_lines)] // Some functions are inherently complex
97#![allow(clippy::needless_pass_by_value)] // Some functions designed for ownership transfer
98#![allow(clippy::similar_names)] // Many similar variable names in algorithms
99#![allow(clippy::unused_async)] // Public API functions may need async for consistency
100#![allow(clippy::needless_range_loop)] // Range loops sometimes clearer than iterators
101#![allow(clippy::uninlined_format_args)] // Explicit argument names can improve clarity
102#![allow(clippy::manual_clamp)] // Manual clamping sometimes clearer
103#![allow(clippy::return_self_not_must_use)] // Not all builder methods need must_use
104#![allow(clippy::cast_possible_wrap)] // Controlled wrapping in processing code
105#![allow(clippy::cast_lossless)] // Explicit casts preferred for clarity
106#![allow(clippy::wildcard_imports)] // Prelude imports are convenient and standard
107#![allow(clippy::format_push_string)] // Sometimes more readable than alternative
108#![allow(clippy::redundant_closure_for_method_calls)] // Closures sometimes needed for type inference
109#![allow(clippy::too_many_arguments)] // Some functions naturally need many parameters
110#![allow(clippy::field_reassign_with_default)] // Sometimes clearer than builder pattern
111#![allow(clippy::trivially_copy_pass_by_ref)] // API consistency more important
112#![allow(clippy::await_holding_lock)] // Controlled lock holding in async contexts
113
114use parking_lot::Mutex;
115use std::{
116    collections::HashMap,
117    ffi::{CStr, CString},
118    os::raw::{c_char, c_float, c_int, c_uint},
119    ptr,
120    sync::Arc,
121};
122use voirs_sdk::{
123    audio::AudioBuffer,
124    error::{Result, VoirsError},
125    types::{AudioFormat, LanguageCode, QualityLevel, SynthesisConfig},
126    VoirsPipeline as SdkPipeline,
127};
128
129pub mod c_api;
130pub mod config;
131pub mod error;
132pub mod memory;
133#[cfg(feature = "nodejs")]
134pub mod nodejs;
135pub mod performance;
136pub mod platform;
137#[cfg(feature = "python")]
138pub mod python;
139pub mod threading;
140pub mod types;
141pub mod utils;
142#[cfg(feature = "wasm")]
143pub mod wasm;
144
145// Note: perf module contains x86_64-specific optimizations
146// and is not compiled by default. See src/perf/ for implementation details.
147
148// Re-export for convenience
149pub use c_api::*;
150pub use error::*;
151pub use performance::*;
152pub use types::*;
153pub use utils::audio::VoirsAudioAnalysis;
154
155// Export Python module when feature is enabled
156// Python types are exported directly from the python module
157#[cfg(feature = "python")]
158pub use python::{PyAudioBuffer, PySynthesisConfig, PyVoiceInfo, VoirsPipeline as PyVoirsPipeline};
159
160// Export Node.js module when feature is enabled
161#[cfg(feature = "nodejs")]
162pub use nodejs::napi_bindings::*;
163
164// Export WASM module when feature is enabled
165#[cfg(feature = "wasm")]
166pub use wasm::wasm_bindings::*;
167
168/// FFI-safe error codes
169#[repr(C)]
170#[derive(Debug, Clone, Copy, PartialEq)]
171pub enum VoirsErrorCode {
172    Success = 0,
173    InvalidParameter = 1,
174    InitializationFailed = 2,
175    SynthesisFailed = 3,
176    VoiceNotFound = 4,
177    IoError = 5,
178    OutOfMemory = 6,
179    OperationCancelled = 7,
180    InternalError = 99,
181}
182
183/// FFI-safe audio format enum
184#[repr(C)]
185#[derive(Debug, Clone, Copy, PartialEq)]
186pub enum VoirsAudioFormat {
187    Wav = 0,
188    Flac = 1,
189    Mp3 = 2,
190    Opus = 3,
191    Ogg = 4,
192}
193
194impl From<AudioFormat> for VoirsAudioFormat {
195    fn from(format: AudioFormat) -> Self {
196        match format {
197            AudioFormat::Wav => Self::Wav,
198            AudioFormat::Flac => Self::Flac,
199            AudioFormat::Mp3 => Self::Mp3,
200            AudioFormat::Opus => Self::Opus,
201            AudioFormat::Ogg => Self::Ogg,
202        }
203    }
204}
205
206impl From<VoirsAudioFormat> for AudioFormat {
207    fn from(format: VoirsAudioFormat) -> Self {
208        match format {
209            VoirsAudioFormat::Wav => Self::Wav,
210            VoirsAudioFormat::Flac => Self::Flac,
211            VoirsAudioFormat::Mp3 => Self::Mp3,
212            VoirsAudioFormat::Opus => Self::Opus,
213            VoirsAudioFormat::Ogg => Self::Ogg,
214        }
215    }
216}
217
218/// FFI-safe quality level enum
219#[repr(C)]
220#[derive(Debug, Clone, Copy, PartialEq)]
221pub enum VoirsQualityLevel {
222    Low = 0,
223    Medium = 1,
224    High = 2,
225    Ultra = 3,
226}
227
228impl From<QualityLevel> for VoirsQualityLevel {
229    fn from(quality: QualityLevel) -> Self {
230        match quality {
231            QualityLevel::Low => Self::Low,
232            QualityLevel::Medium => Self::Medium,
233            QualityLevel::High => Self::High,
234            QualityLevel::Ultra => Self::Ultra,
235        }
236    }
237}
238
239impl From<VoirsQualityLevel> for QualityLevel {
240    fn from(quality: VoirsQualityLevel) -> Self {
241        match quality {
242            VoirsQualityLevel::Low => Self::Low,
243            VoirsQualityLevel::Medium => Self::Medium,
244            VoirsQualityLevel::High => Self::High,
245            VoirsQualityLevel::Ultra => Self::Ultra,
246        }
247    }
248}
249
250/// FFI-safe synthesis configuration
251#[repr(C)]
252#[derive(Debug, Clone)]
253pub struct VoirsSynthesisConfig {
254    pub speaking_rate: c_float,
255    pub pitch_shift: c_float,
256    pub volume_gain: c_float,
257    pub enable_enhancement: c_int, // 0 = false, 1 = true
258    pub output_format: VoirsAudioFormat,
259    pub sample_rate: c_uint,
260    pub quality: VoirsQualityLevel,
261}
262
263impl Default for VoirsSynthesisConfig {
264    fn default() -> Self {
265        let config = SynthesisConfig::default();
266        Self {
267            speaking_rate: config.speaking_rate,
268            pitch_shift: config.pitch_shift,
269            volume_gain: config.volume_gain,
270            enable_enhancement: if config.enable_enhancement { 1 } else { 0 },
271            output_format: config.output_format.into(),
272            sample_rate: config.sample_rate,
273            quality: config.quality.into(),
274        }
275    }
276}
277
278impl From<VoirsSynthesisConfig> for SynthesisConfig {
279    fn from(config: VoirsSynthesisConfig) -> Self {
280        Self {
281            speaking_rate: config.speaking_rate,
282            pitch_shift: config.pitch_shift,
283            volume_gain: config.volume_gain,
284            enable_enhancement: config.enable_enhancement != 0,
285            output_format: config.output_format.into(),
286            sample_rate: config.sample_rate,
287            quality: config.quality.into(),
288            language: LanguageCode::EnUs,  // Default language
289            effects: Vec::new(),           // No effects by default
290            streaming_chunk_size: None,    // Use default chunk size
291            seed: None,                    // No seed by default
292            enable_emotion: false,         // No emotion by default
293            emotion_type: None,            // No emotion type
294            emotion_intensity: 0.7,        // Default intensity
295            emotion_preset: None,          // No preset
296            auto_emotion_detection: false, // No auto detection
297            enable_cloning: false,
298            cloning_method: None,
299            cloning_quality: 0.85,
300            enable_conversion: false,
301            conversion_target: None,
302            realtime_conversion: false,
303            enable_singing: false,
304            singing_voice_type: None,
305            singing_technique: None,
306            musical_key: None,
307            tempo: None,
308            enable_spatial: false,
309            listener_position: None,
310            hrtf_enabled: false,
311            room_size: None,
312            reverb_level: 0.3,
313        }
314    }
315}
316
317/// FFI-safe audio buffer
318#[repr(C)]
319#[derive(Debug)]
320pub struct VoirsAudioBuffer {
321    pub samples: *mut c_float,
322    pub length: c_uint,
323    pub sample_rate: c_uint,
324    pub channels: c_uint,
325    pub duration: c_float,
326}
327
328impl VoirsAudioBuffer {
329    /// Create from Rust AudioBuffer
330    pub fn from_audio_buffer(audio: AudioBuffer) -> Self {
331        let samples = audio.samples().to_vec();
332        let length = samples.len() as c_uint;
333        let sample_rate = audio.sample_rate();
334        let channels = audio.channels();
335        let duration = audio.duration();
336
337        // Allocate C-compatible buffer
338        let mut c_samples = samples.into_boxed_slice();
339        let samples_ptr = c_samples.as_mut_ptr();
340        std::mem::forget(c_samples); // Prevent deallocation
341
342        Self {
343            samples: samples_ptr,
344            length,
345            sample_rate,
346            channels,
347            duration,
348        }
349    }
350
351    /// Convert to Rust AudioBuffer
352    ///
353    /// # Safety
354    ///
355    /// This function is unsafe because it dereferences raw pointers.
356    /// The caller must ensure that:
357    /// - `self.samples` is a valid pointer to at least `self.length` f32 values
358    /// - The memory referenced by `self.samples` remains valid for the duration of this call
359    pub unsafe fn to_audio_buffer(&self) -> AudioBuffer {
360        let samples = std::slice::from_raw_parts(self.samples, self.length as usize).to_vec();
361        AudioBuffer::new(samples, self.sample_rate, self.channels)
362    }
363
364    /// Free the audio buffer
365    ///
366    /// # Safety
367    ///
368    /// This function is unsafe because it deallocates raw memory.
369    /// The caller must ensure that:
370    /// - `self.samples` was allocated using the same allocator as used by this library
371    /// - This function is called at most once per buffer
372    /// - The buffer is not used after calling this function
373    pub unsafe fn free(&mut self) {
374        if !self.samples.is_null() {
375            // Reconstruct the original boxed slice that was forgotten during creation
376            // This is safe because we know the samples pointer came from into_boxed_slice()
377            let boxed_slice = Box::from_raw(std::ptr::slice_from_raw_parts_mut(
378                self.samples,
379                self.length as usize,
380            ));
381            // Drop the boxed slice to deallocate the memory
382            drop(boxed_slice);
383            self.samples = ptr::null_mut();
384        }
385    }
386}
387
388use once_cell::sync::Lazy;
389
390/// Global pipeline manager for FFI
391struct PipelineManager {
392    pipelines: HashMap<u32, Arc<SdkPipeline>>,
393    placeholder_pipelines: std::collections::HashSet<u32>, // Track placeholder IDs for benchmarking
394    next_id: u32,
395}
396
397impl PipelineManager {
398    fn new() -> Self {
399        Self {
400            pipelines: HashMap::new(),
401            placeholder_pipelines: std::collections::HashSet::new(),
402            next_id: 1,
403        }
404    }
405
406    fn add_pipeline(&mut self, pipeline: SdkPipeline) -> u32 {
407        let id = self.next_id;
408        self.pipelines.insert(id, Arc::new(pipeline));
409        self.next_id = self.next_id.wrapping_add(1);
410        if self.next_id == 0 {
411            self.next_id = 1; // Skip 0 as it's reserved for errors
412        }
413        id
414    }
415
416    /// Add a placeholder pipeline for benchmarking (doesn't create actual pipeline)
417    fn add_placeholder_pipeline(&mut self) -> u32 {
418        let id = self.next_id;
419        self.placeholder_pipelines.insert(id);
420        self.next_id = self.next_id.wrapping_add(1);
421        if self.next_id == 0 {
422            self.next_id = 1; // Skip 0 as it's reserved for errors
423        }
424        id
425    }
426
427    fn get_pipeline(&self, id: u32) -> Option<Arc<SdkPipeline>> {
428        self.pipelines.get(&id).cloned()
429    }
430
431    fn remove_pipeline(&mut self, id: u32) -> bool {
432        // Remove from both real pipelines and placeholders
433        let removed_real = self.pipelines.remove(&id).is_some();
434        let removed_placeholder = self.placeholder_pipelines.remove(&id);
435        removed_real || removed_placeholder
436    }
437
438    fn is_valid_pipeline(&self, id: u32) -> bool {
439        self.pipelines.contains_key(&id) || self.placeholder_pipelines.contains(&id)
440    }
441
442    fn count(&self) -> usize {
443        self.pipelines.len() + self.placeholder_pipelines.len()
444    }
445}
446
447/// Global pipeline manager instance using once_cell for thread-safe initialization
448static PIPELINE_MANAGER: Lazy<Mutex<PipelineManager>> =
449    Lazy::new(|| Mutex::new(PipelineManager::new()));
450
451thread_local! {
452    static LAST_ERROR: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
453}
454
455/// Set the last error message for the current thread
456pub fn set_last_error(error: String) {
457    LAST_ERROR.with(|e| {
458        *e.borrow_mut() = Some(error);
459    });
460}
461
462/// Clear the last error for the current thread
463fn clear_last_error() {
464    LAST_ERROR.with(|e| {
465        *e.borrow_mut() = None;
466    });
467}
468
469/// Get the last error message for the current thread
470fn get_last_error() -> Option<String> {
471    LAST_ERROR.with(|e| e.borrow().clone())
472}
473
474/// Get the global pipeline manager
475fn get_pipeline_manager() -> &'static Mutex<PipelineManager> {
476    &PIPELINE_MANAGER
477}
478
479/// Global tokio runtime for async operations
480static TOKIO_RUNTIME: Lazy<Mutex<Option<tokio::runtime::Runtime>>> = Lazy::new(|| Mutex::new(None));
481
482/// Get or create the global tokio runtime
483fn get_runtime() -> std::result::Result<tokio::runtime::Handle, VoirsErrorCode> {
484    // First try to get the current runtime handle if we're already in a runtime context
485    if let Ok(handle) = tokio::runtime::Handle::try_current() {
486        return Ok(handle);
487    }
488
489    // If not in a runtime context, create or get our global runtime
490    let mut runtime_guard = TOKIO_RUNTIME.lock();
491    if runtime_guard.is_none() {
492        let rt =
493            tokio::runtime::Runtime::new().map_err(|_| VoirsErrorCode::InitializationFailed)?;
494        *runtime_guard = Some(rt);
495    }
496
497    match runtime_guard.as_ref() {
498        Some(rt) => Ok(rt.handle().clone()),
499        None => Err(VoirsErrorCode::InternalError),
500    }
501}
502
503/// Utility function to convert C string to Rust string
504unsafe fn c_str_to_string(c_str: *const c_char) -> Result<String> {
505    if c_str.is_null() {
506        let err = VoirsError::config_error("Null string pointer");
507        set_last_error(format!("{err}"));
508        return Err(err);
509    }
510
511    CStr::from_ptr(c_str)
512        .to_str()
513        .map(|s| s.to_string())
514        .map_err(|e| {
515            let err = VoirsError::config_error(format!("Invalid UTF-8: {e}"));
516            set_last_error(format!("{err}"));
517            err
518        })
519}
520
521/// Utility function to convert Rust string to C string
522fn string_to_c_str(s: &str) -> *mut c_char {
523    match CString::new(s) {
524        Ok(c_string) => c_string.into_raw(),
525        Err(_) => ptr::null_mut(),
526    }
527}
528
529/// Free a C string allocated by this library
530///
531/// # Safety
532///
533/// This function is unsafe because it deallocates raw memory.
534/// The caller must ensure that:
535/// - `s` was allocated by this library using CString::into_raw() or equivalent
536/// - This function is called at most once per string
537/// - The string is not used after calling this function
538#[no_mangle]
539pub unsafe extern "C" fn voirs_free_string(s: *mut c_char) {
540    if !s.is_null() {
541        let _ = CString::from_raw(s);
542    }
543}
544
545/// Free an audio buffer allocated by this library
546///
547/// # Safety
548///
549/// This function is unsafe because it deallocates raw memory and dereferences raw pointers.
550/// The caller must ensure that:
551/// - `buffer` was allocated by this library
552/// - This function is called at most once per buffer
553/// - The buffer is not used after calling this function
554#[no_mangle]
555pub unsafe extern "C" fn voirs_free_audio_buffer(buffer: *mut VoirsAudioBuffer) {
556    if !buffer.is_null() {
557        (*buffer).free();
558        let _ = Box::from_raw(buffer);
559    }
560}
561
562/// Convert error code to string description
563#[no_mangle]
564pub extern "C" fn voirs_error_message(code: VoirsErrorCode) -> *const c_char {
565    let message = match code {
566        VoirsErrorCode::Success => "Success",
567        VoirsErrorCode::InvalidParameter => "Invalid parameter",
568        VoirsErrorCode::InitializationFailed => "Initialization failed",
569        VoirsErrorCode::SynthesisFailed => "Synthesis failed",
570        VoirsErrorCode::VoiceNotFound => "Voice not found",
571        VoirsErrorCode::IoError => "I/O error",
572        VoirsErrorCode::OutOfMemory => "Out of memory",
573        VoirsErrorCode::OperationCancelled => "Operation cancelled",
574        VoirsErrorCode::InternalError => "Internal error",
575    };
576
577    message.as_ptr() as *const c_char
578}
579
580/// Get the last error message for the current thread
581#[no_mangle]
582pub extern "C" fn voirs_get_last_error() -> *mut c_char {
583    match get_last_error() {
584        Some(error) => string_to_c_str(&error),
585        None => ptr::null_mut(),
586    }
587}
588
589/// Clear the last error for the current thread
590#[no_mangle]
591pub extern "C" fn voirs_clear_error() {
592    clear_last_error();
593}
594
595/// Check if there is a pending error for the current thread
596#[no_mangle]
597pub extern "C" fn voirs_has_error() -> c_int {
598    if get_last_error().is_some() {
599        1
600    } else {
601        0
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608
609    #[test]
610    fn test_error_code_conversion() {
611        assert_eq!(VoirsErrorCode::Success as i32, 0);
612        assert_eq!(VoirsErrorCode::InvalidParameter as i32, 1);
613        assert_eq!(VoirsErrorCode::InitializationFailed as i32, 2);
614        assert_eq!(VoirsErrorCode::SynthesisFailed as i32, 3);
615        assert_eq!(VoirsErrorCode::VoiceNotFound as i32, 4);
616        assert_eq!(VoirsErrorCode::IoError as i32, 5);
617        assert_eq!(VoirsErrorCode::OutOfMemory as i32, 6);
618        assert_eq!(VoirsErrorCode::InternalError as i32, 99);
619    }
620
621    #[test]
622    fn test_audio_format_conversion() {
623        let format = AudioFormat::Wav;
624        let ffi_format: VoirsAudioFormat = format.into();
625        let back_format: AudioFormat = ffi_format.into();
626        assert_eq!(format, back_format);
627
628        // Test all formats
629        let formats = [
630            (AudioFormat::Wav, VoirsAudioFormat::Wav),
631            (AudioFormat::Flac, VoirsAudioFormat::Flac),
632            (AudioFormat::Mp3, VoirsAudioFormat::Mp3),
633            (AudioFormat::Opus, VoirsAudioFormat::Opus),
634            (AudioFormat::Ogg, VoirsAudioFormat::Ogg),
635        ];
636
637        for (original, ffi) in formats {
638            let converted: VoirsAudioFormat = original.into();
639            assert_eq!(converted, ffi);
640            let back: AudioFormat = converted.into();
641            assert_eq!(back, original);
642        }
643    }
644
645    #[test]
646    fn test_quality_level_conversion() {
647        let qualities = [
648            (QualityLevel::Low, VoirsQualityLevel::Low),
649            (QualityLevel::Medium, VoirsQualityLevel::Medium),
650            (QualityLevel::High, VoirsQualityLevel::High),
651            (QualityLevel::Ultra, VoirsQualityLevel::Ultra),
652        ];
653
654        for (original, ffi) in qualities {
655            let converted: VoirsQualityLevel = original.into();
656            assert_eq!(converted, ffi);
657            let back: QualityLevel = converted.into();
658            assert_eq!(back, original);
659        }
660    }
661
662    #[test]
663    fn test_synthesis_config_conversion() {
664        let config = SynthesisConfig::default();
665        let ffi_config: VoirsSynthesisConfig = VoirsSynthesisConfig::default();
666        let back_config: SynthesisConfig = ffi_config.into();
667        assert_eq!(config.speaking_rate, back_config.speaking_rate);
668        assert_eq!(config.pitch_shift, back_config.pitch_shift);
669        assert_eq!(config.volume_gain, back_config.volume_gain);
670        assert_eq!(config.enable_enhancement, back_config.enable_enhancement);
671        assert_eq!(config.output_format, back_config.output_format);
672        assert_eq!(config.sample_rate, back_config.sample_rate);
673        assert_eq!(config.quality, back_config.quality);
674    }
675
676    #[test]
677    fn test_synthesis_config_default() {
678        let config = VoirsSynthesisConfig::default();
679        assert_eq!(config.speaking_rate, 1.0);
680        assert_eq!(config.pitch_shift, 0.0);
681        assert_eq!(config.volume_gain, 0.0);
682        assert_eq!(config.enable_enhancement, 1); // true
683        assert_eq!(config.output_format, VoirsAudioFormat::Wav);
684        assert_eq!(config.sample_rate, 22050);
685        assert_eq!(config.quality, VoirsQualityLevel::High);
686    }
687
688    #[test]
689    fn test_error_message_function() {
690        let message = voirs_error_message(VoirsErrorCode::Success);
691        let c_str = unsafe { CStr::from_ptr(message) };
692        assert_eq!(c_str.to_str().unwrap(), "Success");
693
694        let message = voirs_error_message(VoirsErrorCode::InvalidParameter);
695        let c_str = unsafe { CStr::from_ptr(message) };
696        assert_eq!(c_str.to_str().unwrap(), "Invalid parameter");
697
698        let message = voirs_error_message(VoirsErrorCode::InternalError);
699        let c_str = unsafe { CStr::from_ptr(message) };
700        assert_eq!(c_str.to_str().unwrap(), "Internal error");
701    }
702
703    #[test]
704    fn test_audio_buffer_memory_safety() {
705        // Test creating and freeing audio buffer
706        let samples = vec![1.0, 2.0, 3.0, 4.0];
707        let audio = AudioBuffer::new(samples.clone(), 44100, 1);
708        let mut ffi_buffer = VoirsAudioBuffer::from_audio_buffer(audio);
709
710        assert_eq!(ffi_buffer.length, 4);
711        assert_eq!(ffi_buffer.sample_rate, 44100);
712        assert_eq!(ffi_buffer.channels, 1);
713        assert!(!ffi_buffer.samples.is_null());
714
715        // Test that samples are correctly stored
716        unsafe {
717            let samples_slice =
718                std::slice::from_raw_parts(ffi_buffer.samples, ffi_buffer.length as usize);
719            assert_eq!(samples_slice, &[1.0, 2.0, 3.0, 4.0]);
720        }
721
722        // Test freeing memory
723        unsafe {
724            ffi_buffer.free();
725        }
726        assert!(ffi_buffer.samples.is_null());
727    }
728
729    #[test]
730    fn test_string_conversion_utilities() {
731        let test_string = "Hello, VoiRS!";
732        let c_string = string_to_c_str(test_string);
733
734        assert!(!c_string.is_null());
735
736        unsafe {
737            let converted_back = c_str_to_string(c_string);
738            assert!(converted_back.is_ok());
739            assert_eq!(converted_back.unwrap(), test_string);
740
741            // Free the string
742            voirs_free_string(c_string);
743        }
744    }
745
746    #[test]
747    fn test_null_string_handling() {
748        unsafe {
749            let result = c_str_to_string(std::ptr::null());
750            assert!(result.is_err());
751
752            // Test freeing null string (should not crash)
753            voirs_free_string(std::ptr::null_mut());
754        }
755    }
756
757    #[test]
758    fn test_voice_info_default() {
759        let voice_info = types::VoirsVoiceInfo::default();
760        assert!(voice_info.id.is_null());
761        assert!(voice_info.name.is_null());
762        assert!(voice_info.language.is_null());
763        assert_eq!(voice_info.quality, VoirsQualityLevel::Medium);
764        assert_eq!(voice_info.is_available, 0);
765    }
766
767    #[test]
768    fn test_voice_list_default() {
769        let voice_list = types::VoirsVoiceList::default();
770        assert!(voice_list.voices.is_null());
771        assert_eq!(voice_list.count, 0);
772    }
773
774    #[test]
775    fn test_pipeline_config_default() {
776        let config = types::VoirsPipelineConfig::default();
777        assert_eq!(config.use_gpu, 0);
778        assert_eq!(config.num_threads, 0);
779        assert!(config.cache_dir.is_null());
780        assert!(config.device.is_null());
781    }
782
783    #[test]
784    fn test_ffi_struct_sizes() {
785        // Ensure structs have reasonable sizes for FFI
786        assert!(std::mem::size_of::<VoirsErrorCode>() <= 8);
787        assert!(std::mem::size_of::<VoirsAudioFormat>() <= 8);
788        assert!(std::mem::size_of::<VoirsQualityLevel>() <= 8);
789        assert!(std::mem::size_of::<VoirsSynthesisConfig>() <= 64);
790        assert!(std::mem::size_of::<VoirsAudioBuffer>() <= 64);
791        assert!(std::mem::size_of::<types::VoirsVoiceInfo>() <= 64);
792        assert!(std::mem::size_of::<types::VoirsVoiceList>() <= 16);
793        assert!(std::mem::size_of::<types::VoirsPipelineConfig>() <= 32);
794    }
795
796    #[test]
797    fn test_ffi_struct_alignment() {
798        // Ensure structs are properly aligned for FFI
799        assert_eq!(
800            std::mem::align_of::<VoirsErrorCode>(),
801            std::mem::align_of::<i32>()
802        );
803        assert_eq!(
804            std::mem::align_of::<VoirsAudioFormat>(),
805            std::mem::align_of::<i32>()
806        );
807        assert_eq!(
808            std::mem::align_of::<VoirsQualityLevel>(),
809            std::mem::align_of::<i32>()
810        );
811    }
812
813    #[test]
814    fn test_repr_c_layout() {
815        // Test that repr(C) structs have expected field offsets
816        use std::mem::offset_of;
817
818        // VoirsSynthesisConfig
819        assert_eq!(offset_of!(VoirsSynthesisConfig, speaking_rate), 0);
820        assert_eq!(offset_of!(VoirsSynthesisConfig, pitch_shift), 4);
821        assert_eq!(offset_of!(VoirsSynthesisConfig, volume_gain), 8);
822        assert_eq!(offset_of!(VoirsSynthesisConfig, enable_enhancement), 12);
823
824        // VoirsAudioBuffer
825        assert_eq!(offset_of!(VoirsAudioBuffer, samples), 0);
826        assert_eq!(offset_of!(VoirsAudioBuffer, length), 8);
827        assert_eq!(offset_of!(VoirsAudioBuffer, sample_rate), 12);
828        assert_eq!(offset_of!(VoirsAudioBuffer, channels), 16);
829        assert_eq!(offset_of!(VoirsAudioBuffer, duration), 20);
830    }
831
832    #[test]
833    fn test_enhanced_error_handling() {
834        // Test that error messages are properly stored and retrieved
835        clear_last_error();
836        assert!(!voirs_has_error() != 0);
837
838        set_last_error("Test error message".to_string());
839        assert!(voirs_has_error() != 0);
840
841        let error_msg = voirs_get_last_error();
842        assert!(!error_msg.is_null());
843
844        unsafe {
845            let c_str = std::ffi::CStr::from_ptr(error_msg);
846            assert_eq!(c_str.to_str().unwrap(), "Test error message");
847            voirs_free_string(error_msg);
848        }
849
850        voirs_clear_error();
851        assert!(voirs_has_error() == 0);
852    }
853
854    #[test]
855    fn test_memory_management_integration() {
856        use crate::memory::{check_memory_leaks, get_memory_stats, reset_memory_stats};
857
858        reset_memory_stats();
859        assert!(check_memory_leaks());
860
861        let initial_stats = get_memory_stats();
862        assert_eq!(initial_stats.current_allocations, 0);
863
864        // Test memory tracking via our C API
865        let stats_json = memory::voirs_memory_get_stats();
866        assert!(!stats_json.is_null());
867
868        unsafe {
869            let stats_str = std::ffi::CStr::from_ptr(stats_json).to_str().unwrap();
870            assert!(stats_str.contains("total_allocations"));
871            voirs_free_string(stats_json);
872        }
873
874        let leak_check = memory::voirs_memory_check_leaks();
875        assert_eq!(leak_check, 1); // Should be leak-free
876    }
877
878    #[test]
879    fn test_memory_pool_integration() {
880        use crate::memory::{pool_allocate, pool_deallocate};
881
882        // Test memory pool allocation
883        let buffer1 = pool_allocate(100);
884        assert_eq!(buffer1.len(), 100);
885
886        let buffer2 = pool_allocate(100);
887        assert_eq!(buffer2.len(), 100);
888
889        // Return to pool
890        pool_deallocate(buffer1);
891        pool_deallocate(buffer2);
892
893        // Should reuse from pool
894        let buffer3 = pool_allocate(100);
895        assert_eq!(buffer3.len(), 100);
896    }
897
898    #[test]
899    fn test_ref_counted_audio_buffer() {
900        use crate::memory::RefCountedBuffer;
901
902        let buffer = RefCountedBuffer::new(vec![1.0, 2.0, 3.0, 4.0], 44100, 2);
903        assert_eq!(buffer.data(), &[1.0, 2.0, 3.0, 4.0]);
904        assert_eq!(buffer.sample_rate(), 44100);
905        assert_eq!(buffer.channels(), 2);
906        assert_eq!(buffer.ref_count(), 1);
907
908        let buffer2 = buffer.clone();
909        assert_eq!(buffer.ref_count(), 2);
910        assert_eq!(buffer2.ref_count(), 2);
911
912        drop(buffer2);
913        assert_eq!(buffer.ref_count(), 1);
914    }
915}