use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{self, JoinHandle};
use crate::engine::{EspeakNg, Parameter, SynthEvent};
use crate::error::Result;
const CHUNK_SAMPLES: usize = 4096;
enum Command {
Speak(String),
SetParameter(Parameter, i32),
SetVoice(String),
}
pub struct AsyncChunk<'a> {
pub text: &'a str,
pub samples: &'a [i16],
pub sample_rate: u32,
pub is_final: bool,
pub events: &'a [SynthEvent],
}
#[derive(Default)]
struct State {
queue: VecDeque<Command>,
busy: bool,
cancel: bool,
stop: bool,
}
struct Shared {
state: Mutex<State>,
work_available: Condvar,
became_idle: Condvar,
}
pub struct AsyncSynth {
shared: Arc<Shared>,
worker: Option<JoinHandle<()>>,
}
impl AsyncSynth {
pub fn new<F>(lang: &str, data_dir: Option<PathBuf>, callback: F) -> Result<Self>
where
F: FnMut(AsyncChunk) -> bool + Send + 'static,
{
let engine = match data_dir {
Some(dir) => EspeakNg::with_data_dir(lang, &dir)?,
None => EspeakNg::new(lang)?,
};
let shared = Arc::new(Shared {
state: Mutex::new(State::default()),
work_available: Condvar::new(),
became_idle: Condvar::new(),
});
let worker_shared = Arc::clone(&shared);
let mut callback = callback;
let worker = thread::spawn(move || worker_loop(engine, worker_shared, &mut callback));
Ok(AsyncSynth { shared, worker: Some(worker) })
}
fn enqueue(&self, cmd: Command) {
self.shared.state.lock().unwrap().queue.push_back(cmd);
self.shared.work_available.notify_one();
}
pub fn speak(&self, text: impl Into<String>) {
self.enqueue(Command::Speak(text.into()));
}
pub fn set_parameter(&self, param: Parameter, value: i32) {
self.enqueue(Command::SetParameter(param, value));
}
pub fn set_voice(&self, name: impl Into<String>) {
self.enqueue(Command::SetVoice(name.into()));
}
pub fn cancel(&self) {
let mut st = self.shared.state.lock().unwrap();
st.queue.clear();
st.cancel = true;
drop(st);
self.shared.became_idle.notify_all();
}
pub fn synchronize(&self) {
let mut st = self.shared.state.lock().unwrap();
while !st.queue.is_empty() || st.busy {
st = self.shared.became_idle.wait(st).unwrap();
}
}
pub fn is_speaking(&self) -> bool {
let st = self.shared.state.lock().unwrap();
!st.queue.is_empty() || st.busy
}
}
impl Drop for AsyncSynth {
fn drop(&mut self) {
{
let mut st = self.shared.state.lock().unwrap();
st.stop = true;
st.queue.clear();
}
self.shared.work_available.notify_all();
if let Some(h) = self.worker.take() {
let _ = h.join();
}
}
}
fn worker_loop(
mut engine: EspeakNg,
shared: Arc<Shared>,
callback: &mut dyn FnMut(AsyncChunk) -> bool,
) {
loop {
let cmd = {
let mut st = shared.state.lock().unwrap();
while st.queue.is_empty() && !st.stop {
shared.became_idle.notify_all();
st = shared.work_available.wait(st).unwrap();
}
if st.stop {
return;
}
st.cancel = false;
st.busy = true;
st.queue.pop_front().unwrap()
};
match cmd {
Command::SetParameter(p, v) => engine.set_parameter(p, v),
Command::SetVoice(name) => engine.set_voice(&name),
Command::Speak(text) => {
let mut pending: Option<Vec<i16>> = None;
let rate = engine.sample_rate();
let _ = engine.synth_streaming_with_events(&text, |samples, is_final, events| {
if shared.state.lock().unwrap().cancel {
pending = None;
return true;
}
if let Some(prev) = pending.replace(samples.to_vec()) {
if stream_chunk(&shared, callback, &text, &prev, rate, false, &[]) {
pending = None;
return true;
}
}
if is_final {
let last = pending.take().unwrap_or_default();
return stream_chunk(&shared, callback, &text, &last, rate, true, events);
}
false
});
}
}
let mut st = shared.state.lock().unwrap();
st.busy = false;
if st.queue.is_empty() {
shared.became_idle.notify_all();
}
}
}
fn stream_chunk(
shared: &Shared,
callback: &mut dyn FnMut(AsyncChunk) -> bool,
text: &str,
samples: &[i16],
sample_rate: u32,
last: bool,
events: &[SynthEvent],
) -> bool {
let n = samples.len();
if n == 0 {
if last {
return callback(AsyncChunk { text, samples: &[], sample_rate, is_final: true, events });
}
return false;
}
let mut off = 0;
while off < n {
if shared.state.lock().unwrap().cancel {
return true;
}
let end = (off + CHUNK_SAMPLES).min(n);
let is_final = last && end == n;
let stop = callback(AsyncChunk {
text,
samples: &samples[off..end],
sample_rate,
is_final,
events: if is_final { events } else { &[] },
});
off = end;
if stop {
shared.state.lock().unwrap().queue.clear();
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
fn have_data() -> bool {
std::path::Path::new("espeak-ng-data/en_dict").exists()
}
#[test]
fn streams_utterances_in_chunks() {
if !have_data() {
eprintln!("[SKIP] no en data");
return;
}
let chunks = Arc::new(AtomicUsize::new(0));
let finals = Arc::new(AtomicUsize::new(0));
let total = Arc::new(AtomicUsize::new(0));
let (c, f, t) = (Arc::clone(&chunks), Arc::clone(&finals), Arc::clone(&total));
let synth = AsyncSynth::new("en", None, move |chunk| {
c.fetch_add(1, Ordering::SeqCst);
t.fetch_add(chunk.samples.len(), Ordering::SeqCst);
if chunk.is_final {
f.fetch_add(1, Ordering::SeqCst);
}
false
})
.unwrap();
synth.speak("hello world how are you today");
synth.speak("goodbye");
synth.synchronize();
assert_eq!(finals.load(Ordering::SeqCst), 2, "expected 2 final chunks");
assert!(chunks.load(Ordering::SeqCst) > 2, "long utterance should stream >1 chunk");
let direct: usize = {
let e = EspeakNg::new("en").unwrap();
e.synth("hello world how are you today").unwrap().0.len()
+ e.synth("goodbye").unwrap().0.len()
};
assert_eq!(total.load(Ordering::SeqCst), direct, "streamed samples must match direct synth");
}
#[test]
fn callback_stop_drops_remaining() {
if !have_data() {
return;
}
let count = Arc::new(AtomicUsize::new(0));
let c = Arc::clone(&count);
let synth = AsyncSynth::new("en", None, move |_chunk| {
c.fetch_add(1, Ordering::SeqCst);
true })
.unwrap();
for _ in 0..10 {
synth.speak("the quick brown fox");
}
synth.synchronize();
assert!(count.load(Ordering::SeqCst) < 10, "callback stop did not drop the queue");
}
#[test]
fn set_parameter_applies_to_later_utterances() {
if !have_data() {
return;
}
let per_utt = Arc::new(Mutex::new(Vec::<usize>::new()));
let running = Arc::new(AtomicUsize::new(0));
let (p, r) = (Arc::clone(&per_utt), Arc::clone(&running));
let synth = AsyncSynth::new("en", None, move |chunk| {
r.fetch_add(chunk.samples.len(), Ordering::SeqCst);
if chunk.is_final {
p.lock().unwrap().push(r.swap(0, Ordering::SeqCst));
}
false
})
.unwrap();
synth.speak("hello world"); synth.set_parameter(Parameter::Rate, 90); synth.speak("hello world"); synth.synchronize();
let utts = per_utt.lock().unwrap();
assert_eq!(utts.len(), 2, "expected two utterances: {utts:?}");
assert!(utts[1] > utts[0], "queued SetParameter(slow) did not lengthen later utterance: {utts:?}");
}
}