use crate::engine::{estimate_word_boundaries, TtsEngine};
use crate::types::{TtsError, TtsResult, Voice};
use std::sync::Mutex;
#[derive(Debug)]
pub struct SystemEngine {
conn: Mutex<Option<speech_dispatcher::Connection>>,
}
impl SystemEngine {
pub fn new() -> Self {
let conn = speech_dispatcher::Connection::open(
"rust-tts-wrapper",
"rust-tts-wrapper",
"rust-tts-wrapper",
speech_dispatcher::Mode::Threaded,
)
.ok();
SystemEngine {
conn: Mutex::new(conn),
}
}
}
fn rate_to_spd(rate: f32) -> i32 {
((rate.clamp(0.1, 10.0) - 1.0) * 100.0).round() as i32
}
fn pitch_to_spd(pitch: f32) -> i32 {
((pitch.clamp(0.1, 10.0) - 1.0) * 100.0).round() as i32
}
fn volume_to_spd(volume: f32) -> i32 {
((volume.clamp(0.0, 2.0) - 1.0) * 100.0).round() as i32
}
impl TtsEngine for SystemEngine {
fn speak(
&self,
text: &str,
voice: Option<&str>,
rate: f32,
pitch: f32,
volume: f32,
_on_audio: Option<crate::engine::OnAudioCallback>,
mut on_boundary: Option<crate::engine::OnBoundaryCallback>,
) -> TtsResult<()> {
let guard = self.conn.lock().unwrap();
let conn = guard
.as_ref()
.ok_or_else(|| TtsError("Speech dispatcher not connected".into()))?;
if let Some(v) = voice {
let _ = conn.set_synthesis_voice_all(v);
}
let _ = conn.set_voice_rate_all(rate_to_spd(rate));
let _ = conn.set_voice_pitch_all(pitch_to_spd(pitch));
let _ = conn.set_volume_all(volume_to_spd(volume));
conn.say(speech_dispatcher::Priority::Important, text);
if let Some(cb) = on_boundary.as_mut() {
let estimated = estimate_word_boundaries(text);
for b in &estimated {
#[allow(clippy::cast_precision_loss)]
let start = b.offset as f32 / 1000.0;
#[allow(clippy::cast_precision_loss)]
let end = (b.offset + b.duration) as f32 / 1000.0;
cb(&b.text, start, end);
}
}
Ok(())
}
fn speak_sync(
&self,
text: &str,
voice: Option<&str>,
rate: f32,
pitch: f32,
volume: f32,
on_audio: Option<crate::engine::OnAudioCallback>,
on_boundary: Option<crate::engine::OnBoundaryCallback>,
) -> TtsResult<()> {
self.speak(text, voice, rate, pitch, volume, on_audio, on_boundary)
}
fn stop(&self) -> TtsResult<()> {
let guard = self.conn.lock().unwrap();
let conn = guard
.as_ref()
.ok_or_else(|| TtsError("Speech dispatcher not connected".into()))?;
conn.cancel()
.map_err(|e| TtsError(format!("Stop failed: {e}")))
}
fn pause(&self) -> TtsResult<()> {
let guard = self.conn.lock().unwrap();
if let Some(conn) = guard.as_ref() {
let _ = conn.pause_all();
}
Ok(())
}
fn resume(&self) -> TtsResult<()> {
let guard = self.conn.lock().unwrap();
if let Some(conn) = guard.as_ref() {
let _ = conn.resume_all();
}
Ok(())
}
fn get_voices(&self) -> TtsResult<Vec<Voice>> {
Ok(vec![])
}
fn engine_id(&self) -> &'static str {
"system"
}
}