#![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.
unused_unsafe // Unsafe is unused in zstr
)]
use std::{
ffi::CStr,
io::{Read, Write},
marker::PhantomData,
os::unix::prelude::{AsRawFd, FromRawFd},
};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use zstr::zstr;
pub use espeakng_sys as bindings;
mod error;
mod structs;
mod utils;
pub use error::{ESpeakNgError, Error};
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 {
let voice_ptr = unsafe { bindings::espeak_GetCurrentVoice() };
assert!(!voice_ptr.is_null(), "voice should not be null");
Voice::from(unsafe { *voice_ptr })
}
#[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 libc::c_char = std::ptr::null();
unsafe {
let version_string = bindings::espeak_Info(std::ptr::addr_of_mut!(c_voice_path));
(
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 {
text_mode,
phoneme_mode,
} => Ok(Some(self.text_to_phonemes_standard(
text,
text_mode,
phoneme_mode,
))),
PhonemeGenOptions::Mbrola | PhonemeGenOptions::MbrolaFile(_) => {
self.text_to_phonemes_mbrola(text, file)
}
}
}
fn text_to_phonemes_standard(
&mut self,
text: &str,
text_mode: TextMode,
phoneme_mode: PhonemeMode,
) -> 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,
text_mode as i32,
phoneme_mode.bits() as i32,
))
};
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() };
}
}