use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use crate::error::{Error, Result};
use crate::synthesize::PcmBuffer;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PhoLine {
pub phoneme: String,
pub duration_ms: u32,
pub pitch: Vec<(u8, u16)>,
}
pub fn write_pho(lines: &[PhoLine]) -> String {
let mut out = String::new();
for l in lines {
out.push_str(&l.phoneme);
out.push(' ');
out.push_str(&l.duration_ms.to_string());
for &(pos, hz) in &l.pitch {
out.push(' ');
out.push_str(&pos.to_string());
out.push(' ');
out.push_str(&hz.to_string());
}
out.push('\n');
}
out
}
pub fn mbrola_database(voice: &str) -> Option<&str> {
voice.strip_prefix("mb-").or_else(|| voice.strip_prefix("mb/"))
}
pub fn find_database(data_dir: &Path, db: &str) -> Result<PathBuf> {
for cand in [
data_dir.join("mbrola").join(db),
data_dir.join("voices").join("mb").join(db),
] {
if cand.exists() {
return Ok(cand);
}
}
Err(Error::MbrolaVoiceNotFound)
}
pub fn synthesize(pho: &str, database: &Path) -> Result<PcmBuffer> {
use std::io::Write;
let mut child = Command::new("mbrola")
.arg(database)
.arg("-")
.arg("-.raw")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.map_err(|_| Error::MbrolaNotFound)?;
child
.stdin
.take()
.ok_or(Error::MbrolaNotFound)?
.write_all(pho.as_bytes())
.map_err(Error::Io)?;
let out = child.wait_with_output().map_err(Error::Io)?;
let pcm = out
.stdout
.chunks_exact(2)
.map(|b| i16::from_le_bytes([b[0], b[1]]))
.collect();
Ok(pcm)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_pho_format() {
let lines = vec![
PhoLine { phoneme: "_".into(), duration_ms: 50, pitch: vec![] },
PhoLine { phoneme: "h".into(), duration_ms: 61, pitch: vec![(0, 120)] },
PhoLine { phoneme: "@".into(), duration_ms: 76, pitch: vec![(0, 118), (50, 115)] },
];
assert_eq!(write_pho(&lines), "_ 50\nh 61 0 120\n@ 76 0 118 50 115\n");
}
#[test]
fn database_resolution() {
assert_eq!(mbrola_database("mb-en1"), Some("en1"));
assert_eq!(mbrola_database("mb/de2"), Some("de2"));
assert_eq!(mbrola_database("en"), None);
assert!(matches!(
find_database(Path::new("/nonexistent"), "en1"),
Err(Error::MbrolaVoiceNotFound)
));
}
#[test]
fn synthesize_without_binary_is_clean() {
let r = synthesize("_ 50\n", Path::new("/nonexistent/en1"));
assert!(r.is_err() || r.map(|p| p.is_empty()).unwrap_or(true));
}
}