use std::sync::Arc;
use tokio::sync::mpsc;
use crate::engine::{EspeakNg, SynthEvent};
use crate::error::Result;
use crate::synthesize::PcmBuffer;
#[derive(Debug, Clone)]
pub struct AudioChunk {
pub samples: PcmBuffer,
pub sample_rate: u32,
pub is_final: bool,
pub events: Vec<SynthEvent>,
}
pub async fn synth(engine: Arc<EspeakNg>, text: impl Into<String>) -> Result<(PcmBuffer, u32)> {
let text = text.into();
match tokio::task::spawn_blocking(move || engine.synth(&text)).await {
Ok(result) => result,
Err(_) => Ok((Vec::new(), 0)),
}
}
pub fn synth_stream(
engine: Arc<EspeakNg>,
text: impl Into<String>,
capacity: usize,
) -> mpsc::Receiver<AudioChunk> {
let text = text.into();
let (tx, rx) = mpsc::channel(capacity.max(1));
tokio::task::spawn_blocking(move || {
let rate = engine.sample_rate();
let _ = engine.synth_streaming_with_events(&text, |samples, is_final, events| {
let chunk = AudioChunk {
samples: samples.to_vec(),
sample_rate: rate,
is_final,
events: events.to_vec(),
};
tx.blocking_send(chunk).is_err()
});
});
rx
}
#[cfg(test)]
mod tests {
use super::*;
fn have_data() -> bool {
std::path::Path::new("espeak-ng-data/en_dict").exists()
}
fn engine() -> Arc<EspeakNg> {
Arc::new(EspeakNg::with_data_dir("en", std::path::Path::new("espeak-ng-data")).unwrap())
}
#[test]
fn synth_awaits_a_whole_utterance() {
if !have_data() {
eprintln!("[SKIP] no en data");
return;
}
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
rt.block_on(async {
let e = engine();
let (pcm, rate) = synth(Arc::clone(&e), "hello world").await.unwrap();
assert_eq!(rate, e.sample_rate());
assert_eq!(pcm, e.synth("hello world").unwrap().0, "async result must match sync");
});
}
#[test]
fn stream_chunks_concatenate_to_the_whole_utterance() {
if !have_data() {
return;
}
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.build()
.unwrap();
rt.block_on(async {
let e = engine();
const TEXT: &str = "One. Two. Three. Four.";
let mut rx = synth_stream(Arc::clone(&e), TEXT, 2);
let mut all = Vec::new();
let mut chunks = 0;
let mut finals = 0;
let mut final_events = 0;
while let Some(c) = rx.recv().await {
chunks += 1;
all.extend_from_slice(&c.samples);
if c.is_final {
finals += 1;
final_events = c.events.len();
}
}
assert!(chunks > 1, "a four-clause utterance should stream >1 chunk");
assert_eq!(finals, 1, "exactly one final chunk");
assert!(final_events > 0, "events are delivered on the final chunk");
assert_eq!(all, e.synth(TEXT).unwrap().0, "streamed audio must match sync");
});
}
#[test]
fn dropping_the_receiver_stops_synthesis() {
if !have_data() {
return;
}
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.build()
.unwrap();
rt.block_on(async {
let e = engine();
let mut rx = synth_stream(e, "One. Two. Three. Four. Five. Six.", 1);
let first = rx.recv().await.expect("at least one chunk");
assert!(!first.samples.is_empty());
drop(rx); });
}
}