use async_trait::async_trait;
use windows::core::HSTRING;
use windows::Media::SpeechSynthesis::SpeechSynthesizer;
use windows::Storage::Streams::DataReader;
use crate::tts::{AudioFormat, Speaker, SynthesizedAudio};
use crate::{Result, VoiceConfig, VoiceError};
#[derive(Debug, Clone)]
pub struct WindowsSpeechSpeaker {
voice: String,
rate: f64,
}
impl WindowsSpeechSpeaker {
pub fn from_config(config: &VoiceConfig) -> Self {
Self {
voice: config.local_tts_voice.clone(),
rate: (config.local_tts_speed as f64).clamp(0.5, 6.0),
}
}
fn synth_blocking(voice: String, rate: f64, text: String) -> Result<Vec<u8>> {
let synth = SpeechSynthesizer::new()
.map_err(|e| VoiceError::Tts(format!("SpeechSynthesizer::new: {e}")))?;
if let Ok(opts) = synth.Options() {
let _ = opts.SetSpeakingRate(rate);
}
if !voice.is_empty() {
if let Ok(all) = SpeechSynthesizer::AllVoices() {
let want = voice.to_lowercase();
let count = all.Size().unwrap_or(0);
for i in 0..count {
if let Ok(info) = all.GetAt(i) {
if let Ok(name) = info.DisplayName() {
if name.to_string_lossy().to_lowercase().contains(&want) {
let _ = synth.SetVoice(&info);
break;
}
}
}
}
}
}
let stream = synth
.SynthesizeTextToStreamAsync(&HSTRING::from(text))
.map_err(|e| VoiceError::Tts(format!("SynthesizeTextToStreamAsync: {e}")))?
.get()
.map_err(|e| VoiceError::Tts(format!("synthesize await: {e}")))?;
let size = stream
.Size()
.map_err(|e| VoiceError::Tts(format!("stream size: {e}")))?;
let input = stream
.GetInputStreamAt(0)
.map_err(|e| VoiceError::Tts(format!("input stream: {e}")))?;
let reader = DataReader::CreateDataReader(&input)
.map_err(|e| VoiceError::Tts(format!("data reader: {e}")))?;
reader
.LoadAsync(size as u32)
.map_err(|e| VoiceError::Tts(format!("load async: {e}")))?
.get()
.map_err(|e| VoiceError::Tts(format!("load await: {e}")))?;
let mut buf = vec![0u8; size as usize];
reader
.ReadBytes(&mut buf)
.map_err(|e| VoiceError::Tts(format!("read bytes: {e}")))?;
Ok(buf)
}
}
#[async_trait]
impl Speaker for WindowsSpeechSpeaker {
async fn synth(&self, text: &str) -> Result<SynthesizedAudio> {
let voice = self.voice.clone();
let rate = self.rate;
let text = text.to_string();
let bytes = tokio::task::spawn_blocking(move || Self::synth_blocking(voice, rate, text))
.await
.map_err(|e| VoiceError::Tts(format!("winrt tts join: {e}")))??;
Ok(SynthesizedAudio {
bytes,
format: AudioFormat::Wav,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn synthesizes_valid_wav() {
let speaker = WindowsSpeechSpeaker {
voice: String::new(),
rate: 1.0,
};
let audio = speaker
.synth("Testing CAR Windows speech synthesis.")
.await
.expect("synthesis should succeed on Windows");
assert!(matches!(audio.format, AudioFormat::Wav));
assert!(
audio.bytes.len() > 1000,
"expected real audio, got {} bytes",
audio.bytes.len()
);
assert_eq!(&audio.bytes[0..4], b"RIFF", "expected RIFF header");
assert_eq!(&audio.bytes[8..12], b"WAVE", "expected WAVE tag");
}
}