use std::process::Command;
fn espeak_bin_path() -> String {
std::env::var("ESPEAK_NG_BIN")
.ok()
.or_else(|| option_env!("ESPEAK_NG_BIN").map(str::to_owned))
.unwrap_or_else(|| "espeak-ng".to_string())
}
fn maybe_data_path() -> Option<String> {
std::env::var("ESPEAK_NG_DATA")
.ok()
.or_else(|| option_env!("ESPEAK_NG_DATA").map(str::to_owned))
}
fn espeak_cmd() -> Command {
let mut cmd = Command::new(espeak_bin_path());
if let Some(data) = maybe_data_path() {
cmd.env("ESPEAK_DATA_PATH", data);
}
cmd
}
#[allow(dead_code)]
pub const ESPEAK_DATA_DIR: &str = "/usr/share/espeak-ng-data";
#[allow(dead_code)]
pub fn data_available() -> bool {
use std::path::Path;
let base = Path::new(ESPEAK_DATA_DIR);
base.join("phontab").exists()
&& base.join("en_dict").exists()
&& base.join("phondata").exists()
}
#[allow(dead_code)]
pub fn espeak_available() -> bool {
espeak_cmd()
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[allow(dead_code)]
pub fn try_espeak_ipa(lang: &str, text: &str) -> Option<String> {
let output = espeak_cmd()
.args(["-v", lang, "-q", "--ipa", "--", text])
.output()
.ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout)
.ok()
.map(|s| s.trim().to_string())
}
#[allow(dead_code)]
pub fn espeak_ipa(lang: &str, text: &str) -> String {
try_espeak_ipa(lang, text)
.unwrap_or_else(|| panic!(
"espeak-ng binary not found or failed \
(lang={lang:?}, text={text:?}). \
Install espeak-ng or skip oracle tests with \
`cargo test --lib` / `cargo test --test encoding_integration`."
))
}
#[allow(dead_code)]
pub fn try_espeak_phonemes(lang: &str, text: &str) -> Option<String> {
let output = espeak_cmd()
.args(["-v", lang, "-q", "-x", "--", text])
.output()
.ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout)
.ok()
.map(|s| s.trim().to_string())
}
#[allow(dead_code)]
pub fn espeak_phonemes(lang: &str, text: &str) -> String {
try_espeak_phonemes(lang, text)
.unwrap_or_else(|| panic!("espeak-ng not found"))
}
#[allow(dead_code)]
pub fn try_espeak_wav(lang: &str, text: &str) -> Option<Vec<u8>> {
static COUNTER: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let tmpfile = std::env::temp_dir().join(format!(
"espeak_oracle_{}_{}.wav",
std::process::id(),
n,
));
let status = espeak_cmd()
.args(["-v", lang, "-w", tmpfile.to_str().unwrap(), "--", text])
.status()
.ok()?;
if !status.success() {
return None;
}
std::fs::read(&tmpfile).ok()
}
#[allow(dead_code)]
pub fn espeak_wav(lang: &str, text: &str) -> Vec<u8> {
try_espeak_wav(lang, text)
.unwrap_or_else(|| panic!("espeak-ng not found"))
}
#[allow(dead_code)]
pub fn wav_to_pcm(wav: &[u8]) -> Vec<i16> {
if wav.len() < 44 {
return Vec::new();
}
let data = &wav[44..];
data.chunks_exact(2)
.map(|chunk| i16::from_le_bytes([chunk[0], chunk[1]]))
.collect()
}
#[allow(dead_code)]
pub fn rms(samples: &[i16]) -> f64 {
if samples.is_empty() {
return 0.0;
}
let sum_sq: f64 = samples.iter().map(|&s| (s as f64).powi(2)).sum();
(sum_sq / samples.len() as f64).sqrt()
}
#[allow(dead_code)]
pub fn snr_db(reference: &[i16], test: &[i16]) -> Option<f64> {
if reference.len() != test.len() {
return None;
}
let signal_power: f64 = reference.iter().map(|&s| (s as f64).powi(2)).sum();
let noise_power: f64 = reference
.iter()
.zip(test.iter())
.map(|(&r, &t)| (r as f64 - t as f64).powi(2))
.sum();
if signal_power == 0.0 {
return Some(f64::INFINITY);
}
if noise_power == 0.0 {
return Some(f64::INFINITY);
}
Some(10.0 * (signal_power / noise_power).log10())
}