feat: Tts can now be used as a component, and multiple TTS instances are supported.

This commit is contained in:
Nolan Darilek 2024-12-05 16:16:23 -06:00
parent 0ef7579f91
commit 3891f289d8
2 changed files with 87 additions and 31 deletions

View File

@ -6,11 +6,22 @@ fn main() {
.add_plugins((DefaultPlugins, bevy_tts::TtsPlugin)) .add_plugins((DefaultPlugins, bevy_tts::TtsPlugin))
.add_systems(Startup, setup) .add_systems(Startup, setup)
.add_systems(Update, (event_poll, greet)) .add_systems(Update, (event_poll, greet))
.add_observer(|trigger: Trigger<TtsEvent>| {
println!("{:?}", trigger.event());
})
.run(); .run();
} }
// Speaks a bunch of messages and changes TTS properties. // Speaks a bunch of messages and changes TTS properties.
fn setup(mut tts: ResMut<Tts>) { fn setup(mut commands: Commands, mut tts: ResMut<Tts>) {
let mut other_tts = Tts::default();
let Features { voice, .. } = other_tts.supported_features();
if voice {
let voices = other_tts.voices().unwrap();
let v = voices.last().unwrap();
other_tts.set_voice(&v.clone()).unwrap();
}
commands.spawn(other_tts);
tts.speak("Hello, world.", false).unwrap(); tts.speak("Hello, world.", false).unwrap();
let Features { rate, .. } = tts.supported_features(); let Features { rate, .. } = tts.supported_features();
if rate { if rate {
@ -57,6 +68,11 @@ fn setup(mut tts: ResMut<Tts>) {
tts.set_volume(original_volume).unwrap(); tts.set_volume(original_volume).unwrap();
} }
tts.speak("Press G for a greeting.", false).unwrap(); tts.speak("Press G for a greeting.", false).unwrap();
tts.speak(
"Press S to speak with a second voice, if you're lucky.",
false,
)
.unwrap();
} }
// Reports events from TTS subsystem. // Reports events from TTS subsystem.
@ -67,8 +83,15 @@ fn event_poll(mut events: EventReader<TtsEvent>) {
} }
// Shows how to output speech in response to a keypress. // Shows how to output speech in response to a keypress.
fn greet(input: Res<ButtonInput<KeyCode>>, mut tts: ResMut<Tts>) { fn greet(input: Res<ButtonInput<KeyCode>>, mut tts: ResMut<Tts>, mut speaker: Query<&mut Tts>) {
if input.just_pressed(KeyCode::KeyG) { if input.just_pressed(KeyCode::KeyG) {
tts.speak("Hey there!", true).unwrap(); tts.speak("Hey there!", true).unwrap();
} }
if input.just_pressed(KeyCode::KeyS) {
if let Ok(mut speaker) = speaker.get_single_mut() {
speaker
.speak("Hey there from the TTS component!", true)
.unwrap();
}
}
} }

View File

@ -1,10 +1,20 @@
use bevy::prelude::*; use bevy::{
ecs::{component::ComponentId, world::DeferredWorld},
prelude::*,
};
use crossbeam_channel::{unbounded, Receiver}; use crossbeam_channel::{unbounded, Receiver};
pub use tts::{self, Backends, Error, Features, UtteranceId}; pub use tts::{self, Backends, Error, Features, UtteranceId};
#[derive(Resource, Clone, Deref, DerefMut)] #[derive(Component, Resource, Clone, Deref, DerefMut)]
#[component(on_add = on_tts_added, on_remove=on_tts_removed)]
pub struct Tts(pub tts::Tts); pub struct Tts(pub tts::Tts);
impl Default for Tts {
fn default() -> Self {
Self(tts::Tts::default().unwrap())
}
}
impl Tts { impl Tts {
pub fn screen_reader_available() -> bool { pub fn screen_reader_available() -> bool {
tts::Tts::screen_reader_available() tts::Tts::screen_reader_available()
@ -18,45 +28,68 @@ pub enum TtsEvent {
UtteranceStop(UtteranceId), UtteranceStop(UtteranceId),
} }
#[derive(Resource)] #[derive(Component, Resource)]
struct TtsChannel(Receiver<TtsEvent>); struct TtsChannel(Receiver<TtsEvent>);
fn poll_callbacks(channel: Res<TtsChannel>, mut events: EventWriter<TtsEvent>) { fn on_tts_added(mut world: DeferredWorld, entity: Entity, _: ComponentId) {
let tts = &world.get::<Tts>(entity).unwrap();
let channel = setup_tts(tts);
world.commands().entity(entity).insert(channel);
}
fn on_tts_removed(mut world: DeferredWorld, entity: Entity, _: ComponentId) {
world.commands().entity(entity).remove::<TtsChannel>();
}
fn poll_callbacks(
mut commands: Commands,
channel: Res<TtsChannel>,
mut events: EventWriter<TtsEvent>,
speakers: Query<(Entity, &TtsChannel), With<Tts>>,
) {
if let Ok(msg) = channel.0.try_recv() { if let Ok(msg) = channel.0.try_recv() {
events.send(msg); events.send(msg);
} }
for (entity, channel) in &speakers {
if let Ok(msg) = channel.0.try_recv() {
commands.entity(entity).trigger(msg);
}
}
} }
fn setup_tts(tts: &Tts) -> TtsChannel {
let (tx, rx) = unbounded();
let tx_begin = tx.clone();
let tx_end = tx.clone();
let tx_stop = tx;
let Features {
utterance_callbacks,
..
} = tts.supported_features();
if utterance_callbacks {
tts.on_utterance_begin(Some(Box::new(move |utterance| {
tx_begin.send(TtsEvent::UtteranceBegin(utterance)).unwrap();
})))
.unwrap();
tts.on_utterance_end(Some(Box::new(move |utterance| {
tx_end.send(TtsEvent::UtteranceEnd(utterance)).unwrap();
})))
.unwrap();
tts.on_utterance_stop(Some(Box::new(move |utterance| {
tx_stop.send(TtsEvent::UtteranceStop(utterance)).unwrap();
})))
.unwrap();
}
TtsChannel(rx)
}
pub struct TtsPlugin; pub struct TtsPlugin;
impl Plugin for TtsPlugin { impl Plugin for TtsPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
let tts = tts::Tts::default().unwrap(); let tts = Tts::default();
let (tx, rx) = unbounded(); let channel = setup_tts(&tts);
let tx_begin = tx.clone();
let tx_end = tx.clone();
let tx_stop = tx;
let Features {
utterance_callbacks,
..
} = tts.supported_features();
if utterance_callbacks {
tts.on_utterance_begin(Some(Box::new(move |utterance| {
tx_begin.send(TtsEvent::UtteranceBegin(utterance)).unwrap();
})))
.unwrap();
tts.on_utterance_end(Some(Box::new(move |utterance| {
tx_end.send(TtsEvent::UtteranceEnd(utterance)).unwrap();
})))
.unwrap();
tts.on_utterance_stop(Some(Box::new(move |utterance| {
tx_stop.send(TtsEvent::UtteranceStop(utterance)).unwrap();
})))
.unwrap();
}
let tts = Tts(tts);
app.add_event::<TtsEvent>() app.add_event::<TtsEvent>()
.insert_resource(TtsChannel(rx)) .insert_resource(channel)
.insert_resource(tts) .insert_resource(tts)
.add_systems(Update, poll_callbacks); .add_systems(Update, poll_callbacks);
} }