#![warn(unsafe_op_in_unsafe_fn)]
#![warn(clippy::pedantic)]
#![allow(
clippy::cast_sign_loss, clippy::cast_possible_wrap, // Simple `as` conversions that will not fail.
clippy::unused_self, // Speaker needs to take self to keep thread safe.
)]
use std::{
io::{Write, Read}, os::unix::prelude::{FromRawFd, AsRawFd},
marker::PhantomData,
ffi::CStr,
};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use zstr::zstr;
pub use espeakng_sys as bindings;
mod error;
mod utils;
mod structs;
pub use error::{Error, ESpeakNgError};
pub use structs::*;
use error::handle_error;
use crate::utils::StringFromCPtr;
pub type Result<T> = std::result::Result<T, Error>;
type AudioBuffer = Mutex<Vec<i16>>;
static SPEAKER: OnceCell<Mutex<Speaker>> = OnceCell::new();
pub fn initialise(voice_path: Option<&str>) -> Result<&'static Mutex<Speaker>> {
SPEAKER.get_or_try_init(|| {
Speaker::initialise(voice_path).map(Mutex::new)
})
}
pub fn get() -> Option<&'static Mutex<Speaker>> {
SPEAKER.get()
}
pub struct Speaker {
_marker: PhantomData<std::cell::Cell<()>>
}
impl Speaker {
pub const DEFAULT_VOICE: &'static str = "gmw/en";
fn initialise(voice_path: Option<&str>) -> Result<Self> {
unsafe extern "C" fn synth_callback(wav: *mut i16, sample_count: i32, events: *mut bindings::espeak_EVENT) -> i32 {
match std::panic::catch_unwind(|| {
if wav.is_null() || sample_count == 0 {
return 0
}
let mut new_ptr = events;
let terminate_event = loop {
let event = unsafe {*new_ptr};
if event.type_ != bindings::espeak_EVENT_TYPE_espeakEVENT_LIST_TERMINATED {
break event
}
new_ptr = unsafe {new_ptr.add(1)};
};
unsafe {
if let Some(audio_buffer) = *(terminate_event.user_data as *const Option<&AudioBuffer>) {
let wav_slice: &[i16] = std::slice::from_raw_parts_mut(wav, sample_count as usize);
audio_buffer.lock().extend(wav_slice);
}
}
0
}) {
Ok(ret) => ret,
Err(err) => {
eprintln!("Panic during Rust -> C -> Rust callback: {:?}", err);
std::process::abort()
}
}
}
let voice_path = voice_path.map(utils::null_term);
unsafe {
bindings::espeak_SetSynthCallback(Some(synth_callback));
bindings::espeak_ng_InitializePath(match voice_path {
Some(path) => path.as_ptr(),
None => std::ptr::null()
});
handle_error(bindings::espeak_ng_Initialize(std::ptr::null_mut()))?;
handle_error(bindings::espeak_ng_InitializeOutput(1, 0, std::ptr::null()))?;
}
let mut self_ = Self {_marker: PhantomData};
self_.set_voice_raw(Speaker::DEFAULT_VOICE)?;
Ok(self_)
}
#[must_use] pub fn get_current_voice(&self) -> Voice {
unsafe {
std::ptr::NonNull::new(bindings::espeak_GetCurrentVoice())
.map(|ptr| Voice::from(*ptr.as_ptr()))
.expect("Voice has somehow been unset!")
}
}
#[must_use] pub fn get_voices() -> Vec<Voice> {
let mut array = unsafe {bindings::espeak_ListVoices(std::ptr::null_mut())};
let mut buf = Vec::new();
unsafe {loop {
let next = array.read();
if next.is_null() {
break buf
}
buf.push(Voice::from(*next));
array = array.add(1);
}}
}
pub fn set_voice(&mut self, voice: &Voice) -> Result<()> {
self.set_voice_raw(&voice.filename)
}
pub fn set_voice_raw(&mut self, filename: &str) -> Result<()> {
let mbrola_voice = filename.starts_with("mb/");
if mbrola_voice {
let mut voice_path = Self::info().1;
voice_path.push(format!("voices/{}", filename));
if !voice_path.exists() {
return Err(Error::ESpeakNg(ESpeakNgError::VoiceNotFound))
}
}
let name_null_term = utils::null_term(filename);
if mbrola_voice {
while let Err(err) = handle_error(unsafe {bindings::espeak_ng_SetVoiceByName(name_null_term.as_ptr())}) {
if let Error::ESpeakNg(espeak_err) = err {
if espeak_err == ESpeakNgError::VoiceNotFound {
continue
}
}
return Err(err)
};
} else {
handle_error(unsafe {bindings::espeak_ng_SetVoiceByName(name_null_term.as_ptr())})?;
}
Ok(())
}
pub fn get_parameter(&mut self, param: Parameter, default: bool) -> i32 {
unsafe {bindings::espeak_GetParameter(
param as u32,
i32::from(!default)
)}
}
pub fn set_parameter(&mut self, param: Parameter, new_value: i32, relative: bool) -> Result<()> {
handle_error(unsafe {
bindings::espeak_ng_SetParameter(
param as u32,
new_value,
i32::from(relative)
)
})
}
#[must_use] pub fn info() -> (String, std::path::PathBuf) {
let mut c_voice_path: *const i8 = std::ptr::null();
unsafe {
let version_string = bindings::espeak_Info((&mut c_voice_path) as *mut *const i8);
(
String::from_cptr(version_string),
std::path::PathBuf::from(String::from_cptr(c_voice_path))
)
}
}
fn _synthesize(&mut self, text: &str, user_data: Option<&AudioBuffer>) -> Result<()> {
let text_nul_term = utils::null_term(text);
handle_error (unsafe {
bindings::espeak_ng_Synthesize(
text_nul_term.as_ptr().cast::<std::ffi::c_void>(),
text_nul_term.len() as u64,
0,
bindings::espeak_POSITION_TYPE_POS_CHARACTER,
0,
bindings::espeakCHARS_UTF8,
std::ptr::null_mut(),
(&user_data.map(|ud| ud as *const _) as *const _) as *mut std::ffi::c_void
)
})?;
handle_error(unsafe {bindings::espeak_ng_Synchronize()})?;
Ok(())
}
pub fn synthesize(&mut self, text: &str) -> Result<Vec<i16>> {
let audio_buffer: AudioBuffer = Mutex::new(Vec::<i16>::new());
self._synthesize(text, Some(&audio_buffer))?;
Ok(audio_buffer.into_inner())
}
pub fn synthesize_to_file(&mut self, file: &mut std::fs::File, text: &str) -> Result<()> {
let audio_data_i16 = self.synthesize(text)?;
let audio_data: Vec<u8> = audio_data_i16.into_iter().flat_map(i16::to_le_bytes).collect();
file.write_all(&audio_data)?;
Ok(())
}
pub fn text_to_phonemes(&mut self, text: &str, option: PhonemeGenOptions) -> Result<Option<String>> {
let file = match option {
PhonemeGenOptions::MbrolaFile(file) => Some(file),
_ => None
};
match option {
PhonemeGenOptions::Standard => Ok(Some(self.text_to_phonemes_standard(text))),
PhonemeGenOptions::Mbrola | PhonemeGenOptions::MbrolaFile(_) => self.text_to_phonemes_mbrola(text, file),
}
}
fn text_to_phonemes_standard(&mut self, text: &str) -> String {
let text_nul_term = utils::null_term(text);
let output = unsafe {
CStr::from_ptr(bindings::espeak_TextToPhonemes(
&mut text_nul_term.as_ptr().cast() as *mut *const std::ffi::c_void,
bindings::espeakCHARS_UTF8 as i32,
0
))
};
output.to_string_lossy().to_string()
}
fn text_to_phonemes_mbrola(&mut self, text: &str, file: Option<&dyn AsRawFd>) -> Result<Option<String>> {
if !self.get_current_voice().filename.starts_with("mb/") {
return Err(Error::MbrolaWithoutMbrolaVoice)
};
let raw_file_fd = match file {
Some(file) => file.as_raw_fd(),
None => unsafe {libc::memfd_create(zstr!("").as_ptr(), 0)}
};
let raw_file = unsafe {
let raw_file_ptr = bindings::fdopen(raw_file_fd, zstr!("w+").as_ptr());
std::ptr::NonNull::new(raw_file_ptr)
.ok_or_else(|| Error::OtherC(Some(errno::errno())))?
};
unsafe {bindings::espeak_SetPhonemeTrace(
bindings::espeakPHONEMES_MBROLA as i32,
raw_file.as_ptr(),
);}
let result = self._synthesize(text, None);
unsafe {bindings::espeak_SetPhonemeTrace(0, std::ptr::null_mut())};
if file.is_none() {
let mut file = unsafe {
bindings::fseek(raw_file.as_ptr(), 0, 0);
let dup_fd = libc::dup(raw_file_fd);
bindings::fclose(raw_file.as_ptr());
std::fs::File::from_raw_fd(dup_fd)
};
result?;
let mut buf = Vec::new();
file.read_to_end(&mut buf)?;
Ok(Some(String::from_utf8(buf)?))
} else {
unsafe {bindings::fclose(raw_file.as_ptr())};
result.map(|_| None)
}
}
}
impl Drop for Speaker {
fn drop(&mut self) {
unsafe {bindings::espeak_ng_Terminate()};
}
}