diff --git a/Cargo.toml b/Cargo.toml index 35b5b8e..decf774 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ edition = "2021" crate-type = ["lib", "cdylib", "staticlib"] [features] +ffi = ["cbindgen"] speech_dispatcher_0_10 = ["speech-dispatcher/0_10"] speech_dispatcher_0_11 = ["speech-dispatcher/0_11"] default = ["speech_dispatcher_0_11"] @@ -19,6 +20,7 @@ default = ["speech_dispatcher_0_11"] [dependencies] dyn-clonable = "0.9" lazy_static = "1" +libc = {version = "0.2", optional = true} log = "0.4" serde = { version = "1", optional = true, features = ["derive"] } thiserror = "1" @@ -27,6 +29,9 @@ unic-langid = "0.9.0" [dev-dependencies] env_logger = "0.9" +[build-dependencies] +cbindgen = {version = "0.18.0", optional = true} + [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } windows = { version = "0.40", features = ["Foundation", "Foundation_Collections", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } @@ -49,4 +54,4 @@ jni = "0.19" ndk-glue = "0.6" [package.metadata.docs.rs] -no-default-features = true \ No newline at end of file +no-default-features = true diff --git a/Makefile.toml b/Makefile.toml index 97655f3..92df00d 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -1,3 +1,8 @@ +[tasks.gen-c-bindings] +install_crate = "cbindgen" +command = "cbindgen" +args = ["-c", "cbindgen.toml", "-o", "target/release/tts-rs.h"] + [tasks.build-android-example] script = [ "cd examples/android", @@ -30,4 +35,4 @@ args = ["build"] dependencies = ["install-trunk", "install-wasm-bindgen-cli"] cwd = "examples/web" command = "trunk" -args = ["serve"] \ No newline at end of file +args = ["serve"] diff --git a/build.rs b/build.rs index 8b1edc2..c92ffc7 100644 --- a/build.rs +++ b/build.rs @@ -8,4 +8,18 @@ fn main() { println!("cargo:rustc-link-lib=framework=AppKit"); } } + + #[cfg(feature = "ffi")] + generate_c_bindings(); +} + +#[cfg(feature = "ffi")] +fn generate_c_bindings() { + use std::path::PathBuf; + let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let mut header_path: PathBuf = std::env::var("OUT_DIR").unwrap().into(); + header_path.push("tts.h"); + cbindgen::generate(crate_dir) + .unwrap() + .write_to_file(header_path); } diff --git a/cbindgen.toml b/cbindgen.toml new file mode 100644 index 0000000..f31ddbe --- /dev/null +++ b/cbindgen.toml @@ -0,0 +1,15 @@ +language = "c" +cpp_compat = true +include_guard = "TTS_RS_H" +header = "/* SPDXLicenseIdentifier: MIT */" +autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" + +[enum] +rename_variants = "QualifiedScreamingSnakeCase" + +[defines] +"target_os = android" = "__ANDROID__" +"target_os = linux" = "__linux__" +"target_os = macos" = "__APPLE__" +"target_arch = wasm32" = "__EMSCRIPTEN__" +windows = "__WIN32__" diff --git a/src/backends/mod.rs b/src/backends/mod.rs index aad23ca..eefdff6 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -16,6 +16,7 @@ mod appkit; #[cfg(any(target_os = "macos", target_os = "ios"))] mod av_foundation; +/// cbindgen:ignore #[cfg(target_os = "android")] mod android; diff --git a/src/ffi.rs b/src/ffi.rs new file mode 100644 index 0000000..b89cd71 --- /dev/null +++ b/src/ffi.rs @@ -0,0 +1,353 @@ +//! Bindings to this library to allow it to be called from C/C++. + +use std::{ + cell::RefCell, + ffi::{CStr, CString, NulError}, + os::raw::c_char, + ptr, +}; + +use crate::{Backends, Features, Tts, UtteranceId}; + +thread_local! { + /// Stores the last reported error, so it can be retrieved at will from C + static LAST_ERROR: RefCell> = RefCell::new(None); +} + +fn set_last_error>>(err: E) -> Result<(), NulError> { + LAST_ERROR.with(|last| { + *last.borrow_mut() = Some(CString::new(err)?); + Ok(()) + }) +} + +/// Get the last reported error as a const C string. +/// This string will be valid until at least the next call to `tts_get_error`. +/// It is never called internally by the library. +#[no_mangle] +pub extern "C" fn tts_get_error() -> *const c_char { + LAST_ERROR.with(|err| match &*err.borrow() { + Some(e) => e.as_ptr(), + None => ptr::null(), + }) +} + +/// Deallocate the last reported error (if any). +#[no_mangle] +pub extern "C" fn tts_clear_error() { + LAST_ERROR.with(|err| { + *err.borrow_mut() = None; + }); +} + +/// Create a new `Tts` instance with the specified backend. +/// If an error occurs, returns a null pointer, +/// Call `tts_get_error()` for more information about the specific error. +#[no_mangle] +pub extern "C" fn tts_new(backend: Backends) -> *mut Tts { + match Tts::new(backend) { + Ok(tts) => Box::into_raw(Box::new(tts)), + Err(e) => { + set_last_error(e.to_string()).unwrap(); + ptr::null_mut() + } + } +} + +/// Create a new Tts object with the default backend. +/// If an error occurs, returns a null pointer, +/// Call `tts_get_error()` for more information about the specific error. +#[no_mangle] +pub extern "C" fn tts_default() -> *mut Tts { + match Tts::default() { + Ok(tts) => Box::into_raw(Box::new(tts)), + Err(e) => { + set_last_error(e.to_string()).unwrap(); + ptr::null_mut() + } + } +} + +/// Free the memory associated with a Tts object. +/// If `tts` is a null pointer, this function does nothing. +#[no_mangle] +pub unsafe extern "C" fn tts_free(tts: *mut Tts) { + if tts.is_null() { + return; + } + Box::from_raw(tts); // Goes out of scope and is dropped +} + +/// Returns the features supported by this Tts engine in the object specified by `features`. +/// `tts` must be a valid pointer to a Tts object. +/// `features` must be a valid pointer to an uninitialized `Features` object. +#[no_mangle] +pub unsafe extern "C" fn tts_supported_features(tts: *const Tts, features: *mut Features) { + *features = tts.as_ref().unwrap().supported_features() +} + +/// Speaks the specified text, optionally interrupting current speech. +/// If `utterance` is not NULL, , fills it with a pointer to the returned UtteranceId (or NULL if +/// the backend doesn't provide one). +/// Returns true on success, false on error or if `tts` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_speak( + tts: *mut Tts, + text: *const c_char, + interrupt: bool, + utterance: *mut *mut UtteranceId, +) -> bool { + if tts.is_null() { + return false; + } + let text = CStr::from_ptr(text).to_string_lossy().into_owned(); + match tts.as_mut().unwrap().speak(text, interrupt) { + Ok(u) => { + if !utterance.is_null() { + *utterance = match u { + Some(u) => Box::into_raw(Box::new(u)), + None => ptr::null_mut(), + }; + } + return true; + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + return false; + } + } +} + +/// Free the memory associated with an `UtteranceId`. +/// Does nothing if `utterance` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_free_utterance(utterance: *mut UtteranceId) { + if utterance.is_null() { + return; + } + Box::from_raw(utterance); +} + +/// Stops current speech. +/// Returns true on success, false on error or if `tts` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_stop(tts: *mut Tts) -> bool { + if tts.is_null() { + return false; + } + match tts.as_mut().unwrap().stop() { + Ok(_) => true, + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} + +/// Returns the minimum rate for this speech synthesizer. +/// `tts` must be a valid pointer to a Tts object. +#[no_mangle] +pub unsafe extern "C" fn tts_min_rate(tts: *const Tts) -> f32 { + tts.as_ref().unwrap().min_rate() +} + +/// Returns the maximum rate for this speech synthesizer. +/// `tts` must be a valid pointer to a Tts object. +#[no_mangle] +pub unsafe extern "C" fn tts_max_rate(tts: *const Tts) -> f32 { + tts.as_ref().unwrap().max_rate() +} + +/// Returns the normal rate for this speech synthesizer. +/// `tts` must be a valid pointer to a Tts object. +#[no_mangle] +pub unsafe extern "C" fn tts_normal_rate(tts: *const Tts) -> f32 { + tts.as_ref().unwrap().normal_rate() +} + +/// Gets the current speech rate. +/// Returns true on success, false on error (likely that the backend doesn't support rate changes) +/// or if `tts` is NULL. +/// Does nothing if `rate` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_get_rate(tts: *const Tts, rate: *mut f32) -> bool { + if tts.is_null() { + return false; + } + match tts.as_ref().unwrap().get_rate() { + Ok(r) => { + if !rate.is_null() { + *rate = r; + } + true + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} + +/// Sets the desired speech rate. +/// Returns true on success, false on error (likely that the backend doesn't support rate changes) +/// or if `tts` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_set_rate(tts: *mut Tts, rate: f32) -> bool { + if tts.is_null() { + return false; + } + match tts.as_mut().unwrap().set_rate(rate) { + Ok(_) => true, + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} + +/// Returns the minimum pitch for this speech synthesizer. +/// `tts` must be a valid pointer to a Tts object. +#[no_mangle] +pub unsafe extern "C" fn tts_min_pitch(tts: *const Tts) -> f32 { + tts.as_ref().unwrap().min_pitch() +} + +/// Returns the maximum pitch for this speech synthesizer. +/// `tts` must be a valid pointer to a Tts object. +#[no_mangle] +pub unsafe extern "C" fn tts_max_pitch(tts: *const Tts) -> f32 { + tts.as_ref().unwrap().max_pitch() +} + +/// Returns the normal pitch for this speech synthesizer. +/// `tts` must be a valid pointer to a Tts object. +#[no_mangle] +pub unsafe extern "C" fn tts_normal_pitch(tts: *const Tts) -> f32 { + tts.as_ref().unwrap().normal_pitch() +} + +/// Gets the current speech pitch. +/// Returns true on success, false on error (likely that the backend doesn't support pitch changes) +/// or if `tts` is NULL. +/// Does nothing if `pitch` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_get_pitch(tts: *const Tts, pitch: *mut f32) -> bool { + if tts.is_null() { + return false; + } + match tts.as_ref().unwrap().get_pitch() { + Ok(r) => { + if !pitch.is_null() { + *pitch = r; + } + true + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} + +/// Sets the desired speech pitch. +/// Returns true on success, false on error (likely that the backend doesn't support pitch changes) +/// or if `tts` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_set_pitch(tts: *mut Tts, pitch: f32) -> bool { + if tts.is_null() { + return false; + } + match tts.as_mut().unwrap().set_pitch(pitch) { + Ok(_) => true, + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} + +/// Returns the minimum volume for this speech synthesizer. +/// `tts` must be a valid pointer to a Tts object. +#[no_mangle] +pub unsafe extern "C" fn tts_min_volume(tts: *const Tts) -> f32 { + tts.as_ref().unwrap().min_volume() +} + +/// Returns the maximum volume for this speech synthesizer. +/// `tts` must be a valid pointer to a Tts object. +#[no_mangle] +pub unsafe extern "C" fn tts_max_volume(tts: *const Tts) -> f32 { + tts.as_ref().unwrap().max_volume() +} + +/// Returns the normal volume for this speech synthesizer. +/// `tts` must be a valid pointer to a Tts object. +#[no_mangle] +pub unsafe extern "C" fn tts_normal_volume(tts: *const Tts) -> f32 { + tts.as_ref().unwrap().normal_volume() +} + +/// Gets the current speech volume. +/// Returns true on success, false on error (likely that the backend doesn't support volume changes) +/// or if `tts` is NULL. +/// Does nothing if `volume` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_get_volume(tts: *const Tts, volume: *mut f32) -> bool { + if tts.is_null() { + return false; + } + match tts.as_ref().unwrap().get_volume() { + Ok(r) => { + if !volume.is_null() { + *volume = r; + } + true + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} + +/// Sets the desired speech volume. +/// Returns true on success, false on error (likely that the backend doesn't support volume changes) +/// or if `tts` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_set_volume(tts: *mut Tts, volume: f32) -> bool { + if tts.is_null() { + return false; + } + match tts.as_mut().unwrap().set_volume(volume) { + Ok(_) => true, + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} + +/// fills `speaking` with a bool indicating whether this speech synthesizer is speaking. +/// Returns true on success, false on error (likely that the backend doesn't support speaking +/// status) or if `tts` is NULL. +/// If `speaking` is NULL, returns this value instead, meaning you can't tell the difference +/// between an error occuring and the synth not speaking. +#[no_mangle] +pub unsafe extern "C" fn tts_is_speaking(tts: *const Tts, speaking: *mut bool) -> bool { + if tts.is_null() { + return false; + } + match tts.as_ref().unwrap().is_speaking() { + Ok(s) => { + if speaking.is_null() { + return s; + } + *speaking = s; + true + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 8a8df57..dc2a978 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,7 +35,10 @@ use tolk::Tolk; pub use unic_langid::LanguageIdentifier; mod backends; +#[cfg(feature = "ffi")] +pub mod ffi; +#[repr(C)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Backends { @@ -158,6 +161,7 @@ unsafe impl Send for UtteranceId {} unsafe impl Sync for UtteranceId {} +#[repr(C)] #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Features {