#![allow(
clippy::missing_panics_doc,
clippy::not_unsafe_ptr_arg_deref,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::ptr_as_ptr,
clippy::cast_ptr_alignment,
clippy::doc_markdown,
clippy::multiple_crate_versions,
clippy::field_reassign_with_default,
non_camel_case_types,
dead_code
)]
#[cfg(all(feature = "avsynth", target_os = "macos"))]
mod avsynth_engine;
pub mod boundaries;
#[cfg(feature = "omnivoice-local")]
pub mod omnivoice_local_engine;
#[cfg(feature = "omnivoice-local")]
pub mod omnivoice_local_support;
#[cfg(feature = "ssml-expand")]
pub mod ssml_expand;
#[cfg(feature = "cloning")]
pub mod cloning;
#[cfg(any(feature = "cloud", feature = "cloud-core"))]
mod cloud_engine;
#[cfg(feature = "cloud-core")]
pub mod cloud_core {
pub use crate::cloud_engine::{
authorization_header, build_gemini_request, build_google_request, edge_sec_ms_gec_at,
elevenlabs_smd_platform, parse_elevenlabs_alignment, parse_gemini_interaction_audio,
sha256_hex, ssml_to_dialect, CloudConfig, GeminiAudioBlock, SigV4Credentials, SignedUrl,
};
}
pub mod engine;
pub mod factory;
#[cfg(feature = "floravox")]
pub mod floravox_engine;
#[cfg(all(feature = "sapi", target_os = "windows"))]
mod sapi_engine;
#[cfg(feature = "sherpaonnx")]
pub mod sherpaonnx_engine;
pub mod qwen3_local_support;
#[cfg(feature = "qwen3-local")]
pub mod qwen3_local_engine;
#[cfg(all(feature = "system", target_os = "linux"))]
mod system_engine;
pub mod timeline;
pub mod types;
pub mod word_search;
#[cfg(feature = "pocket-timing")]
pub mod pocket;
pub use engine::TtsEngine;
pub use factory::create_engine;
#[cfg(feature = "sherpaonnx")]
pub use sherpaonnx_engine::SherpaOnnxEngine;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
use std::sync::{Arc, Mutex};
type BoxedEngine = Arc<dyn TtsEngine>;
pub type CAudioCb = Option<extern "C" fn(*const u8, usize, *mut std::ffi::c_void)>;
pub type CBoundaryCb =
Option<extern "C" fn(*const c_char, i32, i32, f32, f32, i32, *mut std::ffi::c_void)>;
pub type CMarkCb = Option<extern "C" fn(*const c_char, i32, f32, f32, *mut std::ffi::c_void)>;
pub type CVisemeCb = Option<extern "C" fn(i32, f32, *mut std::ffi::c_void)>;
pub type CVoidCb = Option<extern "C" fn(*mut std::ffi::c_void)>;
pub type CErrorCb = Option<extern "C" fn(*const c_char, *mut std::ffi::c_void)>;
type BoxedAudioCb = Box<dyn FnMut(&[u8])>;
type BoxedBoundaryCb = Box<dyn FnMut(&str, f32, f32, i32, i32, bool)>;
type BoxedMarkCb = Box<dyn FnMut(&str, f32, f32, i32)>;
pub struct tts_ctx {
engine: BoxedEngine,
voice_id: Mutex<Option<String>>,
rate: Mutex<f32>,
pitch: Mutex<f32>,
volume: Mutex<f32>,
last_error: Mutex<CString>,
on_audio: Mutex<AudioCallback>,
on_start: Mutex<VoidCallback>,
on_end: Mutex<VoidCallback>,
on_error: Mutex<ErrorCallback>,
on_boundary: Mutex<BoundaryCallback>,
on_mark: Mutex<MarkCallback>,
on_viseme: Mutex<VisemeCallback>,
}
#[derive(Clone, Copy)]
struct AudioCallback {
cb: CAudioCb,
userdata: *mut std::ffi::c_void,
}
#[derive(Clone, Copy)]
struct VoidCallback {
cb: CVoidCb,
userdata: *mut std::ffi::c_void,
}
#[derive(Clone, Copy)]
struct ErrorCallback {
cb: CErrorCb,
userdata: *mut std::ffi::c_void,
}
#[derive(Clone, Copy)]
struct BoundaryCallback {
cb: CBoundaryCb,
userdata: *mut std::ffi::c_void,
}
#[derive(Clone, Copy)]
struct MarkCallback {
cb: CMarkCb,
userdata: *mut std::ffi::c_void,
}
#[derive(Clone, Copy)]
struct VisemeCallback {
cb: CVisemeCb,
userdata: *mut std::ffi::c_void,
}
unsafe impl Send for AudioCallback {}
unsafe impl Sync for AudioCallback {}
unsafe impl Send for VoidCallback {}
unsafe impl Sync for VoidCallback {}
unsafe impl Send for ErrorCallback {}
unsafe impl Sync for ErrorCallback {}
unsafe impl Send for BoundaryCallback {}
unsafe impl Sync for BoundaryCallback {}
unsafe impl Send for VisemeCallback {}
unsafe impl Sync for VisemeCallback {}
static LAST_ERROR: Mutex<Option<CString>> = Mutex::new(None);
fn set_error(msg: &str) {
let mut guard = LAST_ERROR
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*guard = Some(safe_cstring(msg));
}
macro_rules! ffi_catch {
($expr:expr) => {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| $expr)).unwrap_or(-1)
};
}
fn safe_cstring(s: impl AsRef<str>) -> CString {
let s = s.as_ref();
if let Ok(cs) = CString::new(s) {
return cs;
}
let cleaned: String = s.chars().filter(|&c| c != '\0').collect();
CString::new(cleaned).unwrap_or_default()
}
#[no_mangle]
pub extern "C" fn tts_create(
engine_id: *const c_char,
credentials_json: *const c_char,
) -> *mut tts_ctx {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
tts_create_inner(engine_id, credentials_json)
}));
if let Ok(ptr) = result {
ptr
} else {
set_error("engine creation panicked");
ptr::null_mut()
}
}
fn tts_create_inner(engine_id: *const c_char, credentials_json: *const c_char) -> *mut tts_ctx {
if engine_id.is_null() {
set_error("engine_id is null");
return ptr::null_mut();
}
let engine_id_str = unsafe { CStr::from_ptr(engine_id) }
.to_string_lossy()
.into_owned();
let creds = if credentials_json.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(credentials_json) }
.to_string_lossy()
.into_owned()
};
if let Some(engine) = create_engine(&engine_id_str, &creds) {
let ctx = Box::new(tts_ctx {
engine,
voice_id: Mutex::new(None),
rate: Mutex::new(1.0),
pitch: Mutex::new(1.0),
volume: Mutex::new(1.0),
last_error: Mutex::new(CString::new("").unwrap()),
on_audio: Mutex::new(AudioCallback {
cb: None,
userdata: ptr::null_mut(),
}),
on_start: Mutex::new(VoidCallback {
cb: None,
userdata: ptr::null_mut(),
}),
on_end: Mutex::new(VoidCallback {
cb: None,
userdata: ptr::null_mut(),
}),
on_error: Mutex::new(ErrorCallback {
cb: None,
userdata: ptr::null_mut(),
}),
on_mark: Mutex::new(MarkCallback {
cb: None,
userdata: std::ptr::null_mut(),
}),
on_boundary: Mutex::new(BoundaryCallback {
cb: None,
userdata: ptr::null_mut(),
}),
on_viseme: Mutex::new(VisemeCallback {
cb: None,
userdata: ptr::null_mut(),
}),
});
Box::into_raw(ctx)
} else {
set_error(&format!("Unknown engine: {engine_id_str}"));
ptr::null_mut()
}
}
#[no_mangle]
pub extern "C" fn tts_destroy(ctx: *mut tts_ctx) {
if ctx.is_null() {
return;
}
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let boxed = unsafe { Box::from_raw(ctx) };
let _ = boxed.engine.stop();
drop(boxed);
}));
}
#[no_mangle]
pub extern "C" fn tts_speak(ctx: *mut tts_ctx, text: *const c_char) -> i32 {
tts_speak_impl(ctx, text, false)
}
#[no_mangle]
pub extern "C" fn tts_speak_ssml(ctx: *mut tts_ctx, ssml: *const c_char) -> i32 {
tts_speak_impl(ctx, ssml, true)
}
fn tts_speak_impl(ctx: *mut tts_ctx, text: *const c_char, raw_ssml: bool) -> i32 {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
tts_speak_impl_inner(ctx, text, raw_ssml)
}))
.unwrap_or(-1)
}
#[allow(clippy::too_many_lines)]
fn tts_speak_impl_inner(ctx: *mut tts_ctx, text: *const c_char, raw_ssml: bool) -> i32 {
ffi_catch!({
if ctx.is_null() || text.is_null() {
return -1;
}
let ctx_ref = unsafe { &*ctx };
let text_str = unsafe { CStr::from_ptr(text) }
.to_string_lossy()
.into_owned();
let voice = ctx_ref
.voice_id
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let rate = if raw_ssml {
1.0
} else {
*ctx_ref
.rate
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let pitch = if raw_ssml {
1.0
} else {
*ctx_ref
.pitch
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let volume = if raw_ssml {
1.0
} else {
*ctx_ref
.volume
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let audio = {
*ctx_ref
.on_audio
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let boundary = {
*ctx_ref
.on_boundary
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let mark = {
*ctx_ref
.on_mark
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let mut on_audio_closure: Option<BoxedAudioCb> = match audio.cb {
Some(cb) => Some(Box::new(move |bytes: &[u8]| {
cb(bytes.as_ptr(), bytes.len(), audio.userdata);
})),
None => None,
};
let mut on_boundary_closure: Option<BoxedBoundaryCb> = boundary.cb.map(|cb| {
let ud = boundary.userdata;
let b: BoxedBoundaryCb = Box::new(
move |word: &str,
start: f32,
end: f32,
char_offset: i32,
char_len: i32,
estimated: bool| {
if let Ok(c_word) = CString::new(word) {
cb(
c_word.as_ptr(),
char_offset,
char_len,
start,
end,
i32::from(estimated),
ud,
);
}
},
);
b
});
let mut on_mark_closure: Option<BoxedMarkCb> = mark.cb.map(|cb| {
let ud = mark.userdata;
let b: BoxedMarkCb =
Box::new(move |name: &str, start: f32, end: f32, char_offset: i32| {
if let Ok(c_name) = CString::new(name) {
cb(c_name.as_ptr(), char_offset, start, end, ud);
}
});
b
});
let start_cb = {
*ctx_ref
.on_start
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let end_cb = {
*ctx_ref
.on_end
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let error_cb = {
*ctx_ref
.on_error
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
if let Some(cb) = start_cb.cb {
cb(start_cb.userdata);
}
#[cfg(feature = "cloud")]
{
let vis = {
*ctx_ref
.on_viseme
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let vbox: Option<Box<dyn FnMut(i32, f32)>> = vis.cb.map(|cb| {
let ud = vis.userdata;
Box::new(move |id: i32, off: f32| {
cb(id, off, ud);
}) as Box<dyn FnMut(i32, f32)>
});
crate::cloud_engine::set_viseme_callback(vbox);
}
let engine = &ctx_ref.engine;
let result = engine.speak(
&text_str,
voice.as_deref(),
rate,
pitch,
volume,
on_audio_closure
.as_mut()
.map(|f| &mut **f as &mut dyn FnMut(&[u8])),
on_boundary_closure
.as_mut()
.map(|f| &mut **f as &mut dyn FnMut(&str, f32, f32, i32, i32, bool)),
on_mark_closure
.as_mut()
.map(|f| &mut **f as &mut dyn FnMut(&str, f32, f32, i32)),
);
match result {
Ok(()) => {
if let Some(cb) = end_cb.cb {
cb(end_cb.userdata);
}
0
}
Err(e) => {
let msg = e.to_string();
*ctx_ref
.last_error
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = safe_cstring(msg.clone());
if let Some(cb) = error_cb.cb {
if let Ok(c_msg) = CString::new(msg) {
cb(c_msg.as_ptr(), error_cb.userdata);
}
}
-1
}
}
})
}
#[no_mangle]
#[allow(clippy::too_many_lines)]
pub extern "C" fn tts_speak_sync(ctx: *mut tts_ctx, text: *const c_char) -> i32 {
ffi_catch!({
if ctx.is_null() || text.is_null() {
return -1;
}
let ctx_ref = unsafe { &*ctx };
let text_str = unsafe { CStr::from_ptr(text) }
.to_string_lossy()
.into_owned();
let voice = ctx_ref
.voice_id
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let rate = *ctx_ref
.rate
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let pitch = *ctx_ref
.pitch
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let volume = *ctx_ref
.volume
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let audio = {
*ctx_ref
.on_audio
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let boundary = {
*ctx_ref
.on_boundary
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let mark = {
*ctx_ref
.on_mark
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let mut on_mark_closure: Option<BoxedMarkCb> = mark.cb.map(|cb| {
let ud = mark.userdata;
let b: BoxedMarkCb =
Box::new(move |name: &str, start: f32, end: f32, char_offset: i32| {
if let Ok(c_name) = CString::new(name) {
cb(c_name.as_ptr(), char_offset, start, end, ud);
}
});
b
});
let mut on_audio_closure: Option<BoxedAudioCb> = match audio.cb {
Some(cb) => Some(Box::new(move |bytes: &[u8]| {
cb(bytes.as_ptr(), bytes.len(), audio.userdata);
})),
None => None,
};
let mut on_boundary_closure: Option<BoxedBoundaryCb> = boundary.cb.map(|cb| {
let ud = boundary.userdata;
let b: BoxedBoundaryCb = Box::new(
move |word: &str,
start: f32,
end: f32,
char_offset: i32,
char_len: i32,
estimated: bool| {
if let Ok(c_word) = CString::new(word) {
cb(
c_word.as_ptr(),
char_offset,
char_len,
start,
end,
i32::from(estimated),
ud,
);
}
},
);
b
});
let start_cb = {
*ctx_ref
.on_start
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let end_cb = {
*ctx_ref
.on_end
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let error_cb = {
*ctx_ref
.on_error
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
if let Some(cb) = start_cb.cb {
cb(start_cb.userdata);
}
#[cfg(feature = "cloud")]
{
let vis = {
*ctx_ref
.on_viseme
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
};
let vbox: Option<Box<dyn FnMut(i32, f32)>> = vis.cb.map(|cb| {
let ud = vis.userdata;
Box::new(move |id: i32, off: f32| {
cb(id, off, ud);
}) as Box<dyn FnMut(i32, f32)>
});
crate::cloud_engine::set_viseme_callback(vbox);
}
let engine = &ctx_ref.engine;
let result = engine.speak_sync(
&text_str,
voice.as_deref(),
rate,
pitch,
volume,
on_audio_closure
.as_mut()
.map(|f| &mut **f as &mut dyn FnMut(&[u8])),
on_boundary_closure
.as_mut()
.map(|f| &mut **f as &mut dyn FnMut(&str, f32, f32, i32, i32, bool)),
on_mark_closure
.as_mut()
.map(|f| &mut **f as &mut dyn FnMut(&str, f32, f32, i32)),
);
match result {
Ok(()) => {
if let Some(cb) = end_cb.cb {
cb(end_cb.userdata);
}
0
}
Err(e) => {
let msg = e.to_string();
*ctx_ref
.last_error
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = safe_cstring(msg.clone());
if let Some(cb) = error_cb.cb {
if let Ok(c_msg) = CString::new(msg) {
cb(c_msg.as_ptr(), error_cb.userdata);
}
}
-1
}
}
})
}
#[no_mangle]
pub extern "C" fn tts_stop(ctx: *mut tts_ctx) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() {
return;
}
let ctx_ref = unsafe { &*ctx };
let _ = ctx_ref.engine.stop();
}));
}
#[no_mangle]
pub extern "C" fn tts_get_voices(
ctx: *mut tts_ctx,
out_voices: *mut *mut types::tts_voice,
out_count: *mut i32,
) -> i32 {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
tts_get_voices_inner(ctx, out_voices, out_count)
}));
result.unwrap_or(-1)
}
fn tts_get_voices_inner(
ctx: *mut tts_ctx,
out_voices: *mut *mut types::tts_voice,
out_count: *mut i32,
) -> i32 {
if ctx.is_null() || out_voices.is_null() || out_count.is_null() {
return -1;
}
let ctx_ref = unsafe { &*ctx };
let engine = &ctx_ref.engine;
match engine.get_voices() {
Ok(voices) => {
let len = voices.len();
if len == 0 {
unsafe {
*out_voices = ptr::null_mut();
*out_count = 0;
}
return 0;
}
let Ok(layout) = std::alloc::Layout::array::<types::tts_voice>(len) else {
set_error(&format!("Voice array too large: {len} entries"));
unsafe {
*out_voices = ptr::null_mut();
*out_count = 0;
}
return -1;
};
let arr_ptr = unsafe { std::alloc::alloc(layout).cast::<types::tts_voice>() };
for (i, v) in voices.iter().enumerate() {
unsafe {
let entry = arr_ptr.add(i);
std::ptr::write(
entry,
types::tts_voice {
id: safe_cstring(&v.id).into_raw(),
name: safe_cstring(&v.name).into_raw(),
language: safe_cstring({
let lc = v.language_codes.first();
match lc {
Some(lc) if !lc.display.is_empty() => {
format!("{} [{}]", lc.display, lc.bcp47)
}
_ => v.primary_language().to_string(),
}
})
.into_raw(),
gender: safe_cstring(v.gender.to_string()).into_raw(),
engine: safe_cstring(&v.provider).into_raw(),
},
);
}
}
unsafe {
*out_voices = arr_ptr;
*out_count = len as i32;
}
0
}
Err(e) => {
*ctx_ref
.last_error
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = safe_cstring(e.to_string());
-1
}
}
}
#[no_mangle]
pub extern "C" fn tts_free_voices(voices: *mut types::tts_voice, count: i32) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if voices.is_null() || count <= 0 {
return;
}
for i in 0..count {
unsafe {
let v = voices.add(i as usize);
if !(*v).id.is_null() {
let _ = CString::from_raw((*v).id);
}
if !(*v).name.is_null() {
let _ = CString::from_raw((*v).name);
}
if !(*v).language.is_null() {
let _ = CString::from_raw((*v).language);
}
if !(*v).gender.is_null() {
let _ = CString::from_raw((*v).gender);
}
if !(*v).engine.is_null() {
let _ = CString::from_raw((*v).engine);
}
}
}
let Ok(layout) = std::alloc::Layout::array::<types::tts_voice>(count as usize) else {
return;
};
unsafe {
std::alloc::dealloc(voices.cast::<u8>(), layout);
}
}));
}
#[no_mangle]
pub extern "C" fn tts_set_voice(ctx: *mut tts_ctx, voice_id: *const c_char) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() || voice_id.is_null() {
return;
}
let ctx_ref = unsafe { &*ctx };
let id = unsafe { CStr::from_ptr(voice_id) }
.to_string_lossy()
.into_owned();
*ctx_ref
.voice_id
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(id);
}));
}
#[no_mangle]
pub extern "C" fn tts_set_rate(ctx: *mut tts_ctx, rate: f32) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() {
return;
}
*unsafe { &*ctx }
.rate
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = rate;
}));
}
#[no_mangle]
pub extern "C" fn tts_set_pitch(ctx: *mut tts_ctx, pitch: f32) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() {
return;
}
*unsafe { &*ctx }
.pitch
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = pitch;
}));
}
#[no_mangle]
pub extern "C" fn tts_set_volume(ctx: *mut tts_ctx, volume: f32) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() {
return;
}
*unsafe { &*ctx }
.volume
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = volume;
}));
}
#[no_mangle]
pub extern "C" fn tts_set_on_audio(
ctx: *mut tts_ctx,
cb: CAudioCb,
userdata: *mut std::ffi::c_void,
) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() {
return;
}
let ctx_ref = unsafe { &*ctx };
*ctx_ref
.on_audio
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = AudioCallback { cb, userdata };
}));
}
#[no_mangle]
pub extern "C" fn tts_set_on_boundary(
ctx: *mut tts_ctx,
cb: CBoundaryCb,
userdata: *mut std::ffi::c_void,
) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() {
return;
}
let ctx_ref = unsafe { &*ctx };
*ctx_ref
.on_boundary
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = BoundaryCallback { cb, userdata };
}));
}
#[no_mangle]
pub extern "C" fn tts_set_on_mark(ctx: *mut tts_ctx, cb: CMarkCb, userdata: *mut std::ffi::c_void) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() {
return;
}
let ctx_ref = unsafe { &*ctx };
*ctx_ref
.on_mark
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = MarkCallback { cb, userdata };
}));
}
#[no_mangle]
pub extern "C" fn tts_set_on_viseme(
ctx: *mut tts_ctx,
cb: CVisemeCb,
userdata: *mut std::ffi::c_void,
) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() {
return;
}
let ctx_ref = unsafe { &*ctx };
*ctx_ref
.on_viseme
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = VisemeCallback { cb, userdata };
}));
}
#[no_mangle]
pub extern "C" fn tts_set_on_start(
ctx: *mut tts_ctx,
cb: CVoidCb,
userdata: *mut std::ffi::c_void,
) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() {
return;
}
let ctx_ref = unsafe { &*ctx };
*ctx_ref
.on_start
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = VoidCallback { cb, userdata };
}));
}
#[no_mangle]
pub extern "C" fn tts_set_on_end(ctx: *mut tts_ctx, cb: CVoidCb, userdata: *mut std::ffi::c_void) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() {
return;
}
let ctx_ref = unsafe { &*ctx };
*ctx_ref
.on_end
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = VoidCallback { cb, userdata };
}));
}
#[no_mangle]
pub extern "C" fn tts_set_on_error(
ctx: *mut tts_ctx,
cb: CErrorCb,
userdata: *mut std::ffi::c_void,
) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() {
return;
}
let ctx_ref = unsafe { &*ctx };
*ctx_ref
.on_error
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = ErrorCallback { cb, userdata };
}));
}
#[no_mangle]
pub extern "C" fn tts_get_engine_count() -> i32 {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
factory::engine_count() as i32
}))
.unwrap_or(0)
}
#[no_mangle]
pub extern "C" fn tts_get_engines(
out_engines: *mut *mut types::tts_engine_info,
out_count: *mut i32,
) -> i32 {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if out_engines.is_null() || out_count.is_null() {
return -1;
}
let engines = factory::engine_list();
let count = engines.len();
if count == 0 {
unsafe {
*out_engines = ptr::null_mut();
*out_count = 0;
}
return 0;
}
let Ok(layout) = std::alloc::Layout::array::<types::tts_engine_info>(count) else {
return -1;
};
let ptr = unsafe { std::alloc::alloc(layout) } as *mut types::tts_engine_info;
if ptr.is_null() {
return -1;
}
for (i, e) in engines.iter().enumerate() {
unsafe {
let entry = ptr.add(i);
std::ptr::write(
entry,
types::tts_engine_info {
id: safe_cstring(&e.id).into_raw(),
name: safe_cstring(&e.name).into_raw(),
needs_credentials: e.needs_credentials,
credential_keys_json: safe_cstring(&e.credential_keys_json).into_raw(),
},
);
}
}
unsafe {
*out_engines = ptr;
*out_count = count as i32;
}
0
}))
.unwrap_or(-1)
}
#[no_mangle]
pub extern "C" fn tts_free_engines(engines: *mut types::tts_engine_info, count: i32) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if engines.is_null() || count <= 0 {
return;
}
for i in 0..count {
unsafe {
let e = engines.add(i as usize);
if !(*e).id.is_null() {
let _ = CString::from_raw((*e).id);
}
if !(*e).name.is_null() {
let _ = CString::from_raw((*e).name);
}
if !(*e).credential_keys_json.is_null() {
let _ = CString::from_raw((*e).credential_keys_json);
}
}
}
let Ok(layout) = std::alloc::Layout::array::<types::tts_engine_info>(count as usize) else {
return;
};
unsafe {
std::alloc::dealloc(engines.cast::<u8>(), layout);
}
}));
}
#[no_mangle]
pub extern "C" fn tts_get_last_error(ctx: *mut tts_ctx) -> *const c_char {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if !ctx.is_null() {
let ctx_ref = unsafe { &*ctx };
if let Ok(guard) = ctx_ref.last_error.lock() {
if !guard.is_empty() {
return guard.as_ptr();
}
}
}
let guard = LAST_ERROR
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match guard.as_ref() {
Some(cs) => cs.as_ptr(),
None => ptr::null(),
}
}))
.unwrap_or(ptr::null())
}
#[no_mangle]
pub extern "C" fn tts_pause(ctx: *mut tts_ctx) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() {
return;
}
let ctx_ref = unsafe { &*ctx };
let _ = ctx_ref.engine.pause();
}));
}
#[no_mangle]
pub extern "C" fn tts_resume(ctx: *mut tts_ctx) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() {
return;
}
let ctx_ref = unsafe { &*ctx };
let _ = ctx_ref.engine.resume();
}));
}
#[no_mangle]
pub extern "C" fn tts_synth_to_bytes(
ctx: *mut tts_ctx,
text: *const c_char,
out_bytes: *mut *mut u8,
out_len: *mut usize,
) -> i32 {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if ctx.is_null() || text.is_null() || out_bytes.is_null() || out_len.is_null() {
return -1;
}
let ctx_ref = unsafe { &*ctx };
let text_str = unsafe { CStr::from_ptr(text) }
.to_string_lossy()
.into_owned();
let voice = ctx_ref
.voice_id
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let rate = *ctx_ref
.rate
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let pitch = *ctx_ref
.pitch
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let volume = *ctx_ref
.volume
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let engine = &ctx_ref.engine;
match engine.synth_to_bytes(&text_str, voice.as_deref(), rate, pitch, volume) {
Ok(data) => {
if data.is_empty() {
unsafe {
*out_bytes = ptr::null_mut();
*out_len = 0;
}
return 0;
}
let len = data.len();
let Ok(layout) = std::alloc::Layout::array::<u8>(len) else {
set_error(&format!("Audio buffer too large: {len} bytes"));
return -1;
};
let ptr = unsafe { std::alloc::alloc(layout) };
unsafe {
ptr::copy_nonoverlapping(data.as_ptr(), ptr, len);
*out_bytes = ptr;
*out_len = len;
}
0
}
Err(e) => {
*ctx_ref
.last_error
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
safe_cstring(e.to_string());
-1
}
}
}))
.unwrap_or(-1)
}
#[no_mangle]
pub extern "C" fn tts_free_bytes(bytes: *mut u8, len: usize) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if bytes.is_null() || len == 0 {
return;
}
let Ok(layout) = std::alloc::Layout::array::<u8>(len) else {
return;
};
unsafe {
std::alloc::dealloc(bytes, layout);
}
}));
}
#[cfg(test)]
mod poison_tests {
use super::*;
#[test]
fn poisoned_lock_does_not_break_subsequent_ffi_calls() {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _guard = LAST_ERROR.lock().unwrap();
panic!("deliberate poison");
}));
assert!(LAST_ERROR.is_poisoned());
set_error("still alive");
let p = tts_get_last_error(std::ptr::null_mut());
assert!(
!p.is_null(),
"error must be readable through a poisoned lock"
);
let msg = unsafe { std::ffi::CStr::from_ptr(p) }.to_string_lossy();
assert_eq!(msg, "still alive");
*LAST_ERROR
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
LAST_ERROR.clear_poison();
}
}