Compare commits

...

211 Commits

Author SHA1 Message Date
Nolan Darilek 3bc16f0c6f Bump version and windows dependency. 2024-04-19 09:17:08 -05:00
Nolan Darilek 3c8ae0ae42 Bump version. 2024-02-09 11:54:31 -06:00
Nolan Darilek 07edc20861 Switch `Arc` to `Rc` to appease Clippy. 2024-02-09 11:34:01 -06:00
Nolan Darilek 96a5209a9f Attempt to simplify CI. 2024-02-09 11:29:00 -06:00
Nolan Darilek 20b18949e2 Just build for target since we don't need an APK. 2024-02-09 11:07:11 -06:00
Nolan Darilek f29de0aede Switch to actions/checkout@v4. 2024-02-09 11:03:41 -06:00
Nolan Darilek 9e1476fd36
Merge pull request #49 from subalterngames:speech_dispatcher_voices_panic
Fixed a panic in SpeechDispatcher.voices()
2024-02-09 10:59:04 -06:00
Nolan Darilek 3032fe0fb3
Merge pull request #51 from Enyium:patch-1
Added docs.rs link to `Cargo.toml`
2024-02-09 10:54:59 -06:00
Nolan Darilek edd09c24e7 Switch from cargo-apk to xbuild. 2024-02-09 10:49:08 -06:00
Nolan Darilek b7b4e7dc85 Bump dependencies. 2024-02-09 10:37:55 -06:00
Nolan Darilek f593340051
Merge pull request #50 from MarijnS95:drop-ndk-glue
Drop `ndk-glue` dependency from the main crate
2024-02-09 10:31:54 -06:00
Enyium 12d8e1f532
Added docs.rs link to `Cargo.toml` 2023-12-05 12:28:47 +01:00
Marijn Suijten 2a81dc9b70 Drop `ndk-glue` dependency from the main crate
Commit d42d201 ("Update Android dependencies and example.") correctly
replaces `ndk-glue` with `ndk-context` as a more generic crate to hold on
to a global `JavaVM` and Android `jobject` `Context`, but didn't drop the
unused `ndk-glue` crate from the list of Android dependencies.  This
crate is only used in the example crate, and [shouldn't clobber
downstream crates].

Besides, `ndk-glue` has been deprecated for some time and should be
replaced by `android-activity` in the example in a followup PR.

[shouldn't clobber downstream crates]: https://github.com/emilk/egui/pull/3606/files#r1401313794
2023-11-22 00:12:48 +01:00
Esther Alter a0c6cbaf6a Fixed a panic in SpeechDispatcher.voices() 2023-10-25 11:59:13 -04:00
Nolan Darilek 5c528f1d8e Bump version. 2023-09-09 12:44:17 -05:00
Nolan Darilek 9fb8107acf Eliminate some warnings. 2023-09-09 12:43:43 -05:00
Nolan Darilek 8dabcc99c4 Bump windows dependency. 2023-09-09 12:41:36 -05:00
Nolan Darilek b369fb5614 docs.rs is actually using speech-dispatcher 0.11 now. 2023-04-06 10:19:21 -05:00
Nolan Darilek e6e1cd49bf Attempt to fix docs.rs builds and bump version. 2023-04-05 11:27:49 -05:00
Nolan Darilek e2edc18e6e Bump dependency and version. 2023-04-03 10:08:03 -05:00
Nolan Darilek 3eba940a22 Bump `windows` dependency and version. 2023-03-28 14:20:23 -05:00
Nolan Darilek 7e761e1267 Complete migration to jni 0.21. 2023-03-06 15:25:49 -06:00
Nolan Darilek bf8eb07866 Bump version. 2023-03-06 14:36:48 -06:00
Nolan Darilek f5be2b7657 Bump jni dependency. 2023-03-06 14:34:49 -06:00
Nolan Darilek b7e7ed46dd Appease Clippy. 2023-03-06 14:34:49 -06:00
Nolan Darilek 69eebf2ffa Bump windows dependency. 2023-03-06 14:34:49 -06:00
Nolan Darilek 6c6089daf9
Merge pull request #41 from ninjaboy:fix-macos-inline-play-example
Add support for inline pronounciation
2023-03-06 14:34:30 -06:00
Alexey Stolybko c874607afe Add support for inline pronounciation 2022-12-26 20:07:58 +00:00
Nolan Darilek 2667d4e943 Bump version. 2022-12-02 13:06:02 -06:00
Nolan Darilek 8b506a89e0 ... 2022-12-02 13:00:20 -06:00
Nolan Darilek dcaf5b914d ... 2022-12-02 12:59:33 -06:00
Nolan Darilek 359b1c8053 Obnoxious that I can't just push this directly to the mac and test for some reason... 2022-12-02 12:58:46 -06:00
Nolan Darilek 527b4cd61e Small tweaks. 2022-12-02 12:57:34 -06:00
Nolan Darilek 97fa370dec Refactor AVFoundation to oxilangtag. 2022-12-02 12:52:08 -06:00
Nolan Darilek 915673eec6 Bump dependency. 2022-12-02 12:38:50 -06:00
Nolan Darilek cf72bad59a cargo fmt 2022-11-22 15:44:09 -06:00
Nolan Darilek 246e587f2d Merge branch 'master' into oxilangtag 2022-11-22 15:38:04 -06:00
Nolan Darilek d65d79f8fb More tweaks and simplifications to work around ndk-sys checksum failure. 2022-11-22 15:16:18 -06:00
Nolan Darilek c339d2bee3 Bump editions in exampels, and remove unnecessary dependencies. 2022-11-22 14:55:31 -06:00
Nolan Darilek daaead1dc3 Tweak features used in CI builds. 2022-11-22 14:36:15 -06:00
Nolan Darilek d547d84af0 Merge branch 'master' into oxilangtag 2022-11-22 14:16:08 -06:00
Nolan Darilek f6766ec633 Bump speech-dispatcher dependency and tweak docs.rs build configuration. 2022-11-22 14:15:40 -06:00
Nolan Darilek 8102820f86 These checks should be run before the tag is pushed, so remove them from the release workflow. 2022-11-22 14:13:46 -06:00
Nolan Darilek 3c9a78a953 Install necessary targets and simplify further. 2022-11-22 12:44:43 -06:00
Nolan Darilek 5470b9557d Drop old actions-rs actions which seem to no longer be updated. 2022-11-22 12:36:00 -06:00
Nolan Darilek 6770a2ed58 Eliminate caching. 2022-11-22 12:27:40 -06:00
Nolan Darilek 61d84a2120 Manually install toolchain before cache to eliminate a deprecated action and hopefully fix checksum validation failures. 2022-11-22 12:12:57 -06:00
Nolan Darilek 7a91a1e827 Merge branch 'master' into android 2022-11-22 11:26:04 -06:00
Nolan Darilek e19e5ef0b7 Remove unnecessary return statement. 2022-11-22 11:13:05 -06:00
Nolan Darilek 3e4299d0e6 Update some old actions to newer versions. 2022-11-22 11:04:05 -06:00
Nolan Darilek d42d20189a Update Android dependencies and example. 2022-11-22 10:44:18 -06:00
Nolan Darilek 22ae0ef5a3 Refactor to oxilangtag for language codes, and bump Windows dependency. 2022-11-21 12:00:35 -06:00
Nolan Darilek f5716c48f5 Fix type mismatches after speech-dispatcher update. 2022-10-19 10:28:18 -05:00
Nolan Darilek eb1d13976a Bump version. 2022-10-19 09:58:45 -05:00
Nolan Darilek 259549e21d
Merge pull request #34 from helgoboss:master
#33 Fix AVFoundation crash on macOS when getting voices
2022-10-19 09:58:02 -05:00
Nolan Darilek 3679ad6153
Merge pull request #36 from helgoboss:bug/35-appkit-crash
#35 Fix AppKit crash when interrupting speech
2022-10-19 09:55:15 -05:00
Benjamin Klum 94615a254a #35 Fix AppKit crash when interrupting speech
avoid removing first string when queue already empty
2022-10-16 23:18:19 +02:00
Benjamin Klum ddf96c10aa #33 Remove unnecessary unsafe keyword 2022-10-16 23:15:52 +02:00
Benjamin Klum 3fdd452646 #33 Fix AVFoundation crash on macOS when getting voices
by preventing manual cleanup of non-owned objects
2022-10-16 22:35:03 +02:00
Nolan Darilek b4f48fa439 Bump dependency and version. 2022-10-03 16:59:33 -05:00
Nolan Darilek 919bc4249a Bump dependency and version. 2022-09-21 16:38:09 -05:00
Nolan Darilek 79e59d551f Remove unnecessary version pins. 2022-09-07 12:37:29 -05:00
Nolan Darilek 652367fa8a Build on Ubuntu 22.04. 2022-09-07 12:33:45 -05:00
Nolan Darilek 3c38c783b0 Support speech-dispatcher 0.11 and 0.10, dropping support for 0.9. 2022-09-07 12:33:33 -05:00
Nolan Darilek 1eae827ed1 Add support for speech-dispatcher 0.10.2 and bump version. 2022-08-29 16:50:49 -05:00
Nolan Darilek f404e180e4 Bump version. 2022-07-22 10:13:32 -05:00
Nolan Darilek 7cf80fb64d WinRT: Correctly set voice for case where no utterances are in queue.
Fixes #29
2022-07-22 10:08:13 -05:00
Nolan Darilek b50c5b6b93
Merge pull request #28 from Bear-03/traits
Derive common traits for Gender and Voice
2022-07-22 09:57:49 -05:00
Nolan Darilek 748f07138d Bump version and dependency. 2022-07-21 18:34:22 -05:00
Bear-03 15f28c9af4
Derive common traits for Gender and Voice 2022-07-21 01:25:14 +02:00
Nolan Darilek b3d2b788f7 Bump version and dependency. 2022-06-30 18:15:42 -05:00
Nolan Darilek 5feb8e3186 Constrain version so example builds. 2022-06-14 13:57:53 -05:00
Nolan Darilek 238d7e2cb3 Bump version. 2022-06-14 13:13:00 -05:00
Nolan Darilek 507d0b5418 Replace some `unwrap` calls with `ok_or(Error::OperationFailed)`. 2022-06-14 13:09:50 -05:00
Nolan Darilek 10ac1021ee Switch to line doc comments. 2022-06-13 10:35:32 -05:00
Nolan Darilek 323f129b7b #24: Don't use default features when building on docs.rs. 2022-06-13 10:22:39 -05:00
Nolan Darilek 9b4ae761a0 Bump version and dependency. 2022-05-19 12:03:23 -05:00
Nolan Darilek 40f682080d Bump version. 2022-05-09 08:48:07 -05:00
Nolan Darilek 40e28876b2 Remove unnecessary printlns and link directives. 2022-05-09 08:46:46 -05:00
Nolan Darilek 4079f4b3c4 Fix mismatched gender codes. 2022-05-09 08:44:33 -05:00
Nolan Darilek 4283623723 Bump dependency. 2022-05-07 11:04:22 -05:00
Nolan Darilek 569bb160b8 Try to intercept cases where voice might be nil. 2022-03-31 14:48:03 -05:00
Nolan Darilek 4d01717e75 Fix return type in Tolk backend. 2022-03-31 13:38:39 -05:00
Nolan Darilek da19d5f16c Restore. 2022-03-31 13:37:42 -05:00
Nolan Darilek 822f770ab8 Finish making gender optional. 2022-03-31 13:25:08 -05:00
Nolan Darilek 9bd767629a Remove unspecified gender in favor of `Option`. 2022-03-31 13:18:57 -05:00
Nolan Darilek 2b4251f6fa Don't support voices in AppKit for now. 2022-03-31 13:16:35 -05:00
Nolan Darilek 219cfbbe00 src
Add voices support to AvFoundation backend.
2022-03-31 13:10:38 -05:00
Nolan Darilek 264af78c58 Get example previewing voices even if one can't be gotten. 2022-03-31 13:09:37 -05:00
Nolan Darilek e3542abd7c Stub out methods for now. 2022-03-31 11:52:30 -05:00
Nolan Darilek 55c0fbbd2b Remove unnecessary patch. 2022-03-31 11:06:23 -05:00
Nolan Darilek a0945d7ebb Update example for new API. 2022-03-31 11:04:47 -05:00
Nolan Darilek c627583928 Eliminate a warning. 2022-03-31 11:02:20 -05:00
Nolan Darilek ec6d1f74a1 Add voice stubs, currently a no-op, on Android. 2022-03-31 10:55:49 -05:00
Nolan Darilek b9aa36cb3b Update APIs to support case where getting a voice is supported but the value isn't set. 2022-03-31 10:43:07 -05:00
Nolan Darilek e699f7e5e5 Add voices support to web platform. 2022-03-31 10:39:39 -05:00
Nolan Darilek 3f9e7c22db Restore default features. 2022-03-30 20:24:40 -05:00
Nolan Darilek e4c6f6f23a Add voice stubs to Tolk backend. 2022-03-30 20:22:37 -05:00
Nolan Darilek d4b913908c Eliminate a warning. 2022-03-30 20:18:10 -05:00
Nolan Darilek b1f60811bf Add voice support to WinRT backend. 2022-03-30 20:13:27 -05:00
Nolan Darilek 51cd84a6cd Support setting voice with Speech Dispatcher, and clarify features to indicate where getting current voice isn't supported. 2022-03-30 18:38:25 -05:00
Nolan Darilek 142f2e6b3a Use plain 'ol struct. 2022-03-30 18:07:08 -05:00
Nolan Darilek 1e55c43153 WIP: Use correct type in backend implementation. 2022-03-30 15:13:28 -05:00
Nolan Darilek e56a0da2e5 WIP: Reorganize, and try to get working with Speech Dispatcher. 2022-03-30 12:07:59 -05:00
Nolan Darilek 55f841d887 Merge extra module into main module. 2022-03-30 10:54:30 -05:00
Nolan Darilek c222c087b2 cargo fmt 2022-03-30 10:18:22 -05:00
Nolan Darilek 6057d9c968
Merge pull request #2 from francois-caddet/feature/voices
Add voices feature
2022-03-30 10:17:32 -05:00
francois-caddet 88f4598ec6 Merge branch 'master' into feature/voices 2022-03-20 13:02:37 +01:00
Nolan Darilek acecb1f362 Bump windows dependency and crate version. 2022-03-18 08:47:06 -05:00
Nolan Darilek fd9f5ae60a Branch not needed. 2022-03-10 14:35:46 -06:00
Nolan Darilek b435c89239 Bump version. 2022-03-10 14:14:47 -06:00
Nolan Darilek 3366f93e2b Fix release workflow to not build default features. 2022-03-10 14:14:03 -06:00
Nolan Darilek 539003205e Appease Clippy. 2022-03-10 14:08:13 -06:00
Nolan Darilek 5c9c649505 Also disable default features for Linux when running Clippy. 2022-03-10 14:07:18 -06:00
Nolan Darilek f275e506df Disable default-features on Linux since runners don't have speech-dispatcher 0.10 or greater. 2022-03-10 13:57:05 -06:00
Nolan Darilek ef0a78c745 Bump speech-dispatcher, and support building with multiple versions. 2022-03-10 13:46:14 -06:00
Nolan Darilek cc8fd91c86 Set event callbacks on pre-clone value to ensure that they remain alive. 2022-03-10 11:44:59 -06:00
Nolan Darilek 888e6a3dfa Correctly clean up callback references based on whether `Arc` remains cloned on drop. 2022-03-10 11:12:46 -06:00
Nolan Darilek 5944794980 Add `Arc<RwLock<...>>` for remaining platforms. 2022-03-07 18:31:25 -06:00
Nolan Darilek 31309553bb Ditto for Linux. 2022-03-07 17:56:28 -06:00
Nolan Darilek 00bd5e62ff Switch `TTS` to use `Arc<RwLock<Box<dyn Backend>>>` to address soundness issues. 2022-03-07 17:54:26 -06:00
Nolan Darilek 90f7dae6a1 Bump windows dependency and version. 2022-03-07 10:31:37 -06:00
Nolan Darilek f310306508 Bump version. 2022-02-11 09:20:22 -06:00
Nolan Darilek 9f8b670fe0 Bump dependency. 2022-02-11 09:17:46 -06:00
Nolan Darilek cd2216390c Bump dependency and version. 2022-02-05 09:48:52 -06:00
Nolan Darilek 4466923620 Bump speech-dispatcher dependency. 2022-01-27 10:56:11 -06:00
Nolan Darilek 660072809d Bump version and windows-rs dependency. 2022-01-13 14:28:22 -06:00
Nolan Darilek 1e1c04d4e5 Fix rustfmt error. 2022-01-10 11:10:18 -06:00
Nolan Darilek 050b97fde1 Bump version. 2022-01-10 11:03:18 -06:00
Nolan Darilek dc00aa427f Bump speech-dispatcher dependency and update for new return types. 2022-01-10 11:02:12 -06:00
Nolan Darilek 1f466275cf Bump ndk-glue version once more. 2022-01-10 10:52:44 -06:00
Nolan Darilek 9066d2b005 Make formatting more consistent. 2022-01-10 10:51:18 -06:00
Nolan Darilek cdc225418e Bump Android dependencies. 2022-01-10 10:48:19 -06:00
Nolan Darilek ad785b6536
Merge pull request #14 from AlyoshaVasilieva:ndk-glue
Bump ndk-glue version in Android example
2022-01-10 10:46:32 -06:00
Nolan Darilek 89708d9ef1
Merge pull request #16 from AlyoshaVasilieva:exit-loop
Exit Android initialization loop with error when stuck
2022-01-10 10:43:57 -06:00
Nolan Darilek e3f9ebe431
Merge pull request #18 from saona-raimundo:master
Implementing common traits
2022-01-10 10:34:49 -06:00
Raimundo Saona 114fb55fc9 Fixing macos and ios restrictions 2022-01-04 13:43:32 +01:00
Nolan Darilek 2ea472e196 Bump Windows dependency, crate version, and remove `Debug` derive. 2021-12-28 10:09:54 -06:00
Raimundo Saona 5331bc8daf Undo implementation of serde traits for Error 2021-12-23 11:12:35 +01:00
Raimundo Saona e20170583d Common traits for other structs 2021-12-22 15:38:11 +01:00
Raimundo Saona 9ed03753c2 Common traits for Features 2021-12-22 13:28:00 +01:00
Malloc Voidstar bed6cfa206
Exit Android initialization loop with error when stuck
500ms is fairly arbitrary; my emulator took 35 to run that loop.
2021-12-10 10:47:12 -08:00
Malloc Voidstar 5e9c98b063
Also bump gradle.plugin.com.github.willir.rust:plugin
0.3.3 doesn't work with cargo-ndk 2+
2021-12-10 09:31:21 -08:00
Malloc Voidstar ee8ec97ab4
Bump ndk-glue version in Android example 2021-12-10 09:10:39 -08:00
Nolan Darilek d24d1a6a15 Bump version and dependencies. 2021-12-02 09:24:22 -06:00
Nolan Darilek 94417b5351 Only import from speech_dispatcher when building for Linux. 2021-11-19 09:25:37 -06:00
Nolan Darilek 89fd14d957 Bump windows crate dependency. 2021-11-19 09:24:58 -06:00
Nolan Darilek 47e164a0c8 Support Speech Dispatcher initialization failures, and bump version. 2021-11-19 09:22:05 -06:00
Nolan Darilek 57ffbf0e4f Make windows dependency platform-specific and add alloc feature. 2021-11-16 11:36:24 -06:00
Nolan Darilek 119678ae55 Update to windows 0.27 and bump version. 2021-11-16 11:13:31 -06:00
Nolan Darilek d5bdb9f498 Bump version and dependency. 2021-11-15 08:16:00 -06:00
Nolan Darilek 562489e5af Bump version. 2021-11-08 07:28:07 -06:00
Nolan Darilek c12f328cf2 Bump dependency. 2021-11-08 07:27:35 -06:00
Nolan Darilek f8dbc04c36 Bump dependencies. 2021-11-01 10:39:58 -05:00
Nolan Darilek a703e790ec Bump edition and version. 2021-11-01 10:38:26 -05:00
Nolan Darilek 92538fbdb8 Upgrade windows-rs to 0.23. 2021-11-01 10:36:15 -05:00
Nolan Darilek dc3129b79c Bump version. 2021-05-20 17:08:23 -05:00
Nolan Darilek c4038149a8 Remove a conditional that blocked playback in some circumstances on the WinRT backend. 2021-05-20 17:07:55 -05:00
Nolan Darilek ca7789f157 Bump version and Tolk dependency. 2021-05-20 13:59:02 -05:00
Nolan Darilek d85d56c3ee Bump version to work around Tolk crash. 2021-05-11 23:53:15 -05:00
Nolan Darilek d67bf8344a Bump dependencies. 2021-05-11 20:21:03 -05:00
Nolan Darilek 8f5f58028a Use attributes instead. 2021-05-11 20:18:14 -05:00
Nolan Darilek 86b2e07f15 Bump version. 2021-05-11 19:38:39 -05:00
Nolan Darilek 4088eb12a1 Add ability to detect screen readers. Windows-only for now, and requires the `tolk` feature. 2021-05-11 19:37:56 -05:00
Nolan Darilek 7b8da53d81 Args shouldn't be a list. 2021-04-05 07:45:08 -05:00
Nolan Darilek e4b53d17aa Check web example as part of release process. 2021-04-03 12:59:32 -05:00
Nolan Darilek 316b1bceec Use target as part of toolchain installation. 2021-04-03 12:58:19 -05:00
Nolan Darilek 26d06fc635 No really, build the web example. 2021-04-03 12:11:53 -05:00
Nolan Darilek a879b3dca3 Get web example compiling. 2021-04-03 11:58:23 -05:00
Nolan Darilek d5a692008a Add action to ensure that web example compiles. 2021-04-03 11:50:11 -05:00
Nolan Darilek f7239366f0 Add command to build the web example. 2021-04-03 11:49:44 -05:00
Nolan Darilek debab7de17 Bump version. 2021-04-03 11:11:37 -05:00
Nolan Darilek 1011704b82 And again, VSCode's find/replace didn't catch this. *grumble* 2021-03-31 11:12:42 -05:00
Nolan Darilek d9639c049b S/TTS/Tts/ here as well. 2021-03-31 11:03:14 -05:00
Nolan Darilek 6dbf9b7ddc Find/replace is failing me today. 2021-03-31 11:01:26 -05:00
Nolan Darilek 336c266ed4 Missed a few... 2021-03-31 10:53:08 -05:00
Nolan Darilek 57f91105ec s/TTS/Tts/ as per Clippy's acronym warnings. 2021-03-31 10:40:42 -05:00
Nolan Darilek ef96042b12 Bump Windows dependency and update accordingly. Also, fix an acronym Clippy warning. 2021-03-31 10:38:32 -05:00
Nolan Darilek acccdfeada Bump version. 2021-03-16 17:36:29 -05:00
Nolan Darilek 153075ebab Add web example
Closes #1
2021-03-16 17:33:01 -05:00
Nolan Darilek 25f8211661 Bump version. 2021-03-16 14:18:49 -05:00
Nolan Darilek fb7f1dddfc *sigh* Fix stupid M1/ARM casting issue. I hate Apple. 2021-03-15 14:02:05 -05:00
Nolan Darilek 50528ce2d1 Another comparison check fix. 2021-03-15 13:47:41 -05:00
Nolan Darilek 8c2aae7afd Try another initialization fix. 2021-03-15 13:46:22 -05:00
Nolan Darilek ed2d2e76c3 And this is what happens when I don't test on actual hardware. 2021-03-15 13:06:49 -05:00
Nolan Darilek 45255a8049 Fix another possibly broken comparison. 2021-03-15 13:04:38 -05:00
Nolan Darilek c65c0022d8 (Hopefully) initialize utterances correctly. 2021-03-15 13:03:41 -05:00
Nolan Darilek bd8e2ee20a Compare against ObjC NO to ensure correctness. 2021-03-15 13:02:51 -05:00
Nolan Darilek 00485d6cd8 Enable 'exception' feature to hopefully catch and surface ObjC exceptions. 2021-03-12 08:36:52 -06:00
Nolan Darilek cdfb7ddb77 Even more bloody logging. 2021-03-12 06:59:49 -06:00
Nolan Darilek 290eb06d02 Even more trace logging. 2021-03-12 06:38:46 -06:00
Nolan Darilek e91637a67c Add even more trace logging. 2021-03-12 06:28:02 -06:00
Nolan Darilek 81eba99594 Add cast to (hopefully) get AppKit compiling on M1 macs. 2021-03-12 06:20:05 -06:00
Nolan Darilek 1f510120a5 Add trace logging in AVFoundation backend. 2021-03-12 05:58:30 -06:00
Nolan Darilek 1d075f7ece When speech is interrupted on AVFoundation, only stop if already speaking. May address a possible deadlock. 2021-03-12 05:50:08 -06:00
Nolan Darilek a22ee53727 Fix Clippy warnings. 2021-03-12 05:48:14 -06:00
François Caddet d2c42d97f5 the voices::Backend trait is almost stable 2020-09-28 11:18:54 +02:00
François Caddet 3294a82485 some fixes
now build on macOS
2020-09-27 20:35:40 +02:00
François Caddet e19eb56169 first implementation of a voice trait for macOS
WARN: not tested
2020-09-27 20:04:12 +02:00
François Caddet f7297e18fd add condition for macOS 11 and greater for default backend 2020-09-26 23:39:30 +02:00
François Caddet f78aed211f fix conflicts 2020-09-26 23:36:15 +02:00
Francois Caddet 008662c940 temporary fix to a build issue with the crate speech-dispatcher 2020-09-26 23:16:10 +02:00
Francois Caddet 8c8dc0ae9f add voices value returned by the backends 2020-09-26 23:03:56 +02:00
Francois Caddet 47cbb80595 Merge branch 'develop' into feature/voices 2020-09-26 18:20:10 +02:00
François Caddet 97f1de5724 Merge branch 'develop' into feature/voices 2020-09-05 12:27:19 +02:00
François Caddet 335ac710a6 add unimplemented functions forvoices feature on every backends 2020-09-05 12:07:51 +02:00
François Caddet b238c8c938 fix return type of AVSpeechSynthesisVoice:new 2020-09-05 11:30:11 +02:00
François Caddet 1b8809aaeb remove the example changing voice.
the default() voice working properly for av_foundation
2020-09-05 10:55:23 +02:00
François Caddet 0fb6c62d83 fix some parameters types and implement set_voice
We have an ilegal hardware instruction in
backend::av_foundation::voices::AVSpeechSynthesisVoice::new(identifier)
when sending voiceWithIdentifier. Is it because the runLoop is not
runing when it's called?
2020-09-04 15:48:56 +02:00
François Caddet 6ed94686f3 implement set_voice for AVFoundation backend
- TODO: test the implementation
- fixed: set_voice mutability of self parameter
2020-09-03 18:40:32 +02:00
François Caddet 5b0d1b6621 Add voices feature
Implemented for AVFoundation backend but set_voice has no effect for now
Warning: does not build on Linux or windows for now
2020-09-03 16:50:11 +02:00
31 changed files with 1231 additions and 516 deletions

View File

@ -6,79 +6,16 @@ on:
- "v*"
jobs:
check:
name: Check
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v1
- run: sudo apt-get update; sudo apt-get install -y libspeechd-dev
if: ${{ runner.os == 'Linux' }}
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: rustfmt, clippy
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: --all-features --examples
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
check_web:
name: Check Web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v1
- uses: actions-rs/toolchain@v1
with:
target: wasm32-unknown-unknown
profile: minimal
toolchain: stable
components: rustfmt, clippy
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: --all-features --examples --target wasm32-unknown-unknown
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features --target wasm32-unknown-unknown
publish:
name: Publish
runs-on: ubuntu-latest
needs: [check, check_web]
runs-on: ubuntu-22.04
env:
CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }}
steps:
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v1
- uses: actions-rs/toolchain@v1
with:
target: wasm32-unknown-unknown
profile: minimal
toolchain: stable
override: true
- uses: actions/checkout@v4
- run: |
sudo apt-get update
sudo apt-get install -y libspeechd-dev
cargo login $CARGO_TOKEN
rustup toolchain install stable
cargo publish

View File

@ -5,80 +5,58 @@ on:
pull_request:
jobs:
check_formatting:
name: Check Formatting
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- run: |
rustup toolchain install stable
cargo fmt --all --check
cd examples/web
cargo fmt --all --check
check:
name: Check
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
os: [windows-latest, ubuntu-22.04, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v1
- uses: actions/checkout@v4
- run: sudo apt-get update; sudo apt-get install -y libspeechd-dev
if: ${{ runner.os == 'Linux' }}
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: rustfmt, clippy
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: --all-features --examples
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
- run: |
rustup toolchain install stable
cargo clippy --all-targets
check_web:
name: Check Web
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v1
- uses: actions-rs/toolchain@v1
with:
target: wasm32-unknown-unknown
profile: minimal
toolchain: stable
components: rustfmt, clippy
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: --all-features --examples --target wasm32-unknown-unknown
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features --target wasm32-unknown-unknown
- uses: actions/checkout@v4
- run: |
rustup target add wasm32-unknown-unknown
rustup toolchain install stable
cargo clippy --all-targets --target wasm32-unknown-unknown
check_android:
name: Check Android
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: Swatinem/rust-cache@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: rustfmt, clippy
override: true
- uses: actions-rs/install@v0.1
with:
crate: cargo-apk
# use-tool-cache: true
- run: rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
- uses: actions-rs/cargo@v1
with:
command: apk
args: build
- uses: actions/checkout@v4
- run: |
rustup target add aarch64-linux-android
rustup toolchain install stable
cargo clippy --all-targets --target aarch64-linux-android
check_web_example:
name: Check Web Example
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- run: |
rustup target add wasm32-unknown-unknown
rustup toolchain install stable
cd examples/web
cargo build --target wasm32-unknown-unknown

View File

@ -1,44 +1,71 @@
[package]
name = "tts"
version = "0.15.0"
version = "0.26.1"
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
repository = "https://github.com/ndarilek/tts-rs"
description = "High-level Text-To-Speech (TTS) interface"
documentation = "https://docs.rs/tts"
license = "MIT"
exclude = ["*.cfg", "*.yml"]
edition = "2018"
edition = "2021"
[lib]
crate-type = ["lib", "cdylib", "staticlib"]
[features]
speech_dispatcher_0_9 = ["speech-dispatcher/0_9"]
speech_dispatcher_0_10 = ["speech-dispatcher/0_10"]
speech_dispatcher_0_11 = ["speech-dispatcher/0_11"]
default = ["speech_dispatcher_0_11"]
[dependencies]
dyn-clonable = "0.9"
oxilangtag = "0.1"
lazy_static = "1"
log = "0.4"
serde = { version = "1", optional = true, features = ["derive"] }
thiserror = "1"
[dev-dependencies]
env_logger = "0.8"
env_logger = "0.11"
[target.'cfg(windows)'.dependencies]
tolk = { version = "0.3", optional = true }
windows = "0.4"
[target.'cfg(windows)'.build-dependencies]
windows = "0.4"
tolk = { version = "0.5", optional = true }
windows = { version = "0.56", features = [
"Foundation",
"Foundation_Collections",
"Media_Core",
"Media_Playback",
"Media_SpeechSynthesis",
"Storage_Streams",
] }
[target.'cfg(target_os = "linux")'.dependencies]
speech-dispatcher = "0.7"
speech-dispatcher = { version = "0.16", default-features = false }
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
cocoa-foundation = "0.1"
core-foundation = "0.9"
libc = "0.2"
objc = "0.2"
objc = { version = "0.2", features = ["exception"] }
[target.wasm32-unknown-unknown.dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "SpeechSynthesisErrorCode", "SpeechSynthesisErrorEvent", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "Window", ] }
web-sys = { version = "0.3", features = [
"EventTarget",
"SpeechSynthesis",
"SpeechSynthesisErrorCode",
"SpeechSynthesisErrorEvent",
"SpeechSynthesisEvent",
"SpeechSynthesisUtterance",
"SpeechSynthesisVoice",
"Window",
] }
[target.'cfg(target_os="android")'.dependencies]
jni = "0.19"
ndk-glue = "0.3"
jni = "0.21"
ndk-context = "0.1"
[package.metadata.docs.rs]
no-default-features = true
features = ["speech_dispatcher_0_11"]

View File

@ -12,4 +12,22 @@ script = [
[tasks.log-android]
command = "adb"
args = ["logcat", "RustStdoutStderr:D", "*:S"]
args = ["logcat", "RustStdoutStderr:D", "*:S"]
[tasks.install-trunk]
install_crate = { crate_name = "trunk", binary = "trunk", test_arg = "--help" }
[tasks.install-wasm-bindgen-cli]
install_crate = { crate_name = "wasm-bindgen-cli", binary = "wasm-bindgen", test_arg = "--help" }
[tasks.build-web-example]
dependencies = ["install-trunk", "install-wasm-bindgen-cli"]
cwd = "examples/web"
command = "trunk"
args = ["build"]
[tasks.run-web-example]
dependencies = ["install-trunk", "install-wasm-bindgen-cli"]
cwd = "examples/web"
command = "trunk"
args = ["serve"]

View File

@ -1,14 +1,4 @@
fn main() {
#[cfg(windows)]
if std::env::var("TARGET").unwrap().contains("windows") {
windows::build!(
windows::foundation::{EventRegistrationToken, IAsyncOperation, TypedEventHandler},
windows::media::core::MediaSource,
windows::media::playback::{MediaPlaybackSession, MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory},
windows::media::speech_synthesis::{SpeechSynthesisStream, SpeechSynthesizer, SpeechSynthesizerOptions},
windows::storage::streams::IRandomAccessStream,
);
}
if std::env::var("TARGET").unwrap().contains("-apple") {
println!("cargo:rustc-link-lib=framework=AVFoundation");
if !std::env::var("CARGO_CFG_TARGET_OS")

View File

@ -12,7 +12,7 @@ use tts::*;
fn main() -> Result<(), Error> {
env_logger::init();
let mut tts = TTS::default()?;
let mut tts = Tts::default()?;
let mut bottles = 99;
while bottles > 0 {
tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?;

View File

@ -1,16 +1,16 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "org.mozilla.rust-android-gradle.rust-android"
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
namespace "rs.tts"
compileSdkVersion 33
ndkVersion "25.1.8937393"
defaultConfig {
applicationId "rs.tts"
minSdkVersion 21
targetSdkVersion 30
targetSdkVersion 33
versionCode 1
versionName "1.0"
}
@ -21,27 +21,27 @@ android {
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.core:core-ktx:1.2.0"
implementation "androidx.annotation:annotation:1.1.0"
implementation "com.google.android.material:material:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
}
apply plugin: "com.github.willir.rust.cargo-ndk-android"
apply plugin: "org.mozilla.rust-android-gradle.rust-android"
cargoNdk {
cargo {
module = "."
libname = "tts"
targets = ["arm", "x86"]
}
tasks.whenTaskAdded { task ->
if ((task.name == 'javaPreCompileDebug' || task.name == 'javaPreCompileRelease')) {
task.dependsOn "cargoBuild"
}
}
project.afterEvaluate {

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="rs.tts">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:label="@string/app_name">
<activity android:name=".MainActivity">
<activity android:name=".MainActivity" android:exported="true">
<meta-data android:name="android.app.lib_name" android:value="hello_world" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -1,28 +1,28 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.3.72"
repositories {
google()
jcenter()
buildscript {
repositories {
google()
mavenCentral()
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "gradle.plugin.com.github.willir.rust:plugin:0.3.3"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
}
plugins {
id "com.android.application" version "7.3.0" apply false
id "com.android.library" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.21" apply false
id "org.mozilla.rust-android-gradle.rust-android" version "0.9.3" apply false
}
allprojects {
repositories {
google()
jcenter()
}
}
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir

View File

@ -2,7 +2,7 @@
name = "hello_world"
version = "0.1.0"
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
edition = "2018"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -10,5 +10,5 @@ edition = "2018"
crate-type = ["dylib"]
[dependencies]
ndk-glue = "0.2"
ndk-glue = "0.7"
tts = { path = "../.." }

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip

View File

@ -1 +1,8 @@
include ":app"
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
include ":app"

View File

@ -4,7 +4,7 @@ use tts::*;
// Without it, the `TTS` instance gets dropped before callbacks can run.
#[allow(unreachable_code)]
fn run() -> Result<(), Error> {
let mut tts = TTS::default()?;
let mut tts = Tts::default()?;
let Features {
utterance_callbacks,
..

89
examples/clone_drop.rs Normal file
View File

@ -0,0 +1,89 @@
use std::io;
#[cfg(target_os = "macos")]
use cocoa_foundation::base::id;
#[cfg(target_os = "macos")]
use cocoa_foundation::foundation::NSRunLoop;
#[cfg(target_os = "macos")]
use objc::{msg_send, sel, sel_impl};
use tts::*;
fn main() -> Result<(), Error> {
env_logger::init();
let tts = Tts::default()?;
if Tts::screen_reader_available() {
println!("A screen reader is available on this platform.");
} else {
println!("No screen reader is available on this platform.");
}
let Features {
utterance_callbacks,
..
} = tts.supported_features();
if utterance_callbacks {
tts.on_utterance_begin(Some(Box::new(|utterance| {
println!("Started speaking {:?}", utterance)
})))?;
tts.on_utterance_end(Some(Box::new(|utterance| {
println!("Finished speaking {:?}", utterance)
})))?;
tts.on_utterance_stop(Some(Box::new(|utterance| {
println!("Stopped speaking {:?}", utterance)
})))?;
}
let mut tts_clone = tts.clone();
drop(tts);
let Features { is_speaking, .. } = tts_clone.supported_features();
if is_speaking {
println!("Are we speaking? {}", tts_clone.is_speaking()?);
}
tts_clone.speak("Hello, world.", false)?;
let Features { rate, .. } = tts_clone.supported_features();
if rate {
let original_rate = tts_clone.get_rate()?;
tts_clone.speak(format!("Current rate: {}", original_rate), false)?;
tts_clone.set_rate(tts_clone.max_rate())?;
tts_clone.speak("This is very fast.", false)?;
tts_clone.set_rate(tts_clone.min_rate())?;
tts_clone.speak("This is very slow.", false)?;
tts_clone.set_rate(tts_clone.normal_rate())?;
tts_clone.speak("This is the normal rate.", false)?;
tts_clone.set_rate(original_rate)?;
}
let Features { pitch, .. } = tts_clone.supported_features();
if pitch {
let original_pitch = tts_clone.get_pitch()?;
tts_clone.set_pitch(tts_clone.max_pitch())?;
tts_clone.speak("This is high-pitch.", false)?;
tts_clone.set_pitch(tts_clone.min_pitch())?;
tts_clone.speak("This is low pitch.", false)?;
tts_clone.set_pitch(tts_clone.normal_pitch())?;
tts_clone.speak("This is normal pitch.", false)?;
tts_clone.set_pitch(original_pitch)?;
}
let Features { volume, .. } = tts_clone.supported_features();
if volume {
let original_volume = tts_clone.get_volume()?;
tts_clone.set_volume(tts_clone.max_volume())?;
tts_clone.speak("This is loud!", false)?;
tts_clone.set_volume(tts_clone.min_volume())?;
tts_clone.speak("This is quiet.", false)?;
tts_clone.set_volume(tts_clone.normal_volume())?;
tts_clone.speak("This is normal volume.", false)?;
tts_clone.set_volume(original_volume)?;
}
tts_clone.speak("Goodbye.", false)?;
let mut _input = String::new();
// The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context.
// It shouldn't be needed in an app or game that almost certainly has one already.
#[cfg(target_os = "macos")]
{
let run_loop: id = unsafe { NSRunLoop::currentRunLoop() };
unsafe {
let _: () = msg_send![run_loop, run];
}
}
io::stdin().read_line(&mut _input)?;
Ok(())
}

View File

@ -11,7 +11,12 @@ use tts::*;
fn main() -> Result<(), Error> {
env_logger::init();
let mut tts = TTS::default()?;
let mut tts = Tts::default()?;
if Tts::screen_reader_available() {
println!("A screen reader is available on this platform.");
} else {
println!("No screen reader is available on this platform.");
}
let Features {
utterance_callbacks,
..
@ -66,6 +71,23 @@ fn main() -> Result<(), Error> {
tts.speak("This is normal volume.", false)?;
tts.set_volume(original_volume)?;
}
let Features { voice, .. } = tts.supported_features();
if voice {
let voices = tts.voices()?;
println!("Available voices:\n===");
for v in &voices {
println!("{:?}", v);
}
let Features { get_voice, .. } = tts.supported_features();
let original_voice = if get_voice { tts.voice()? } else { None };
for v in &voices {
tts.set_voice(v)?;
tts.speak(format!("This is {}.", v.name()), false)?;
}
if let Some(original_voice) = original_voice {
tts.set_voice(&original_voice)?;
}
}
tts.speak("Goodbye.", false)?;
let mut _input = String::new();
// The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context.

View File

@ -4,7 +4,7 @@ use tts::*;
fn main() -> Result<(), Error> {
env_logger::init();
let mut tts = TTS::default()?;
let mut tts = Tts::default()?;
println!("Press Enter and wait for speech.");
loop {
let mut _input = String::new();

View File

@ -1,13 +1,30 @@
#[cfg(target_os = "macos")]
use cocoa_foundation::base::id;
#[cfg(target_os = "macos")]
use cocoa_foundation::foundation::NSDefaultRunLoopMode;
#[cfg(target_os = "macos")]
use cocoa_foundation::foundation::NSRunLoop;
#[cfg(target_os = "macos")]
use objc::class;
#[cfg(target_os = "macos")]
use objc::{msg_send, sel, sel_impl};
use std::{thread, time};
use tts::*;
fn main() -> Result<(), Error> {
env_logger::init();
let mut tts = TTS::default()?;
let mut tts = Tts::default()?;
let mut phrase = 1;
loop {
tts.speak(format!("Phrase {}", phrase), false)?;
#[cfg(target_os = "macos")]
{
let run_loop: id = unsafe { NSRunLoop::currentRunLoop() };
unsafe {
let date: id = msg_send![class!(NSDate), distantFuture];
let _: () = msg_send![run_loop, runMode:NSDefaultRunLoopMode beforeDate:date];
}
}
let time = time::Duration::from_secs(5);
thread::sleep(time);
phrase += 1;

View File

@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"

1
examples/web/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist

13
examples/web/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "web"
version = "0.1.0"
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
console_log = "0.2"
log = "0.4"
seed = "0.9"
tts = { path = "../.." }

12
examples/web/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Example</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

157
examples/web/src/main.rs Normal file
View File

@ -0,0 +1,157 @@
#![allow(clippy::wildcard_imports)]
use seed::{prelude::*, *};
use tts::Tts;
#[derive(Clone)]
struct Model {
text: String,
tts: Tts,
}
#[derive(Clone)]
enum Msg {
TextChanged(String),
RateChanged(String),
PitchChanged(String),
VolumeChanged(String),
VoiceChanged(String),
Speak,
}
fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
let mut tts = Tts::default().unwrap();
if tts.voices().unwrap().iter().len() > 0 {
if tts.voice().unwrap().is_none() {
tts.set_voice(tts.voices().unwrap().first().unwrap())
.expect("Failed to set voice");
}
}
Model {
text: "Hello, world. This is a test of the current text-to-speech values.".into(),
tts,
}
}
fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
use Msg::*;
match msg {
TextChanged(text) => model.text = text,
RateChanged(rate) => {
let rate = rate.parse::<f32>().unwrap();
model.tts.set_rate(rate).unwrap();
}
PitchChanged(pitch) => {
let pitch = pitch.parse::<f32>().unwrap();
model.tts.set_pitch(pitch).unwrap();
}
VolumeChanged(volume) => {
let volume = volume.parse::<f32>().unwrap();
model.tts.set_volume(volume).unwrap();
}
VoiceChanged(voice) => {
for v in model.tts.voices().unwrap() {
if v.id() == voice {
model.tts.set_voice(&v).unwrap();
}
}
}
Speak => {
model.tts.speak(&model.text, false).unwrap();
}
}
}
fn view(model: &Model) -> Node<Msg> {
let should_show_voices = model.tts.voices().unwrap().iter().len() > 0;
form![
div![label![
"Text to speak",
input![
attrs! {
At::Value => model.text,
At::AutoFocus => AtValue::None,
},
input_ev(Ev::Input, Msg::TextChanged)
],
],],
div![label![
"Rate",
input![
attrs! {
At::Type => "number",
At::Value => model.tts.get_rate().unwrap(),
At::Min => model.tts.min_rate(),
At::Max => model.tts.max_rate()
},
input_ev(Ev::Input, Msg::RateChanged)
],
],],
div![label![
"Pitch",
input![
attrs! {
At::Type => "number",
At::Value => model.tts.get_pitch().unwrap(),
At::Min => model.tts.min_pitch(),
At::Max => model.tts.max_pitch()
},
input_ev(Ev::Input, Msg::PitchChanged)
],
],],
div![label![
"Volume",
input![
attrs! {
At::Type => "number",
At::Value => model.tts.get_volume().unwrap(),
At::Min => model.tts.min_volume(),
At::Max => model.tts.max_volume()
},
input_ev(Ev::Input, Msg::VolumeChanged)
],
],],
if should_show_voices {
div![
label!["Voice"],
select![
model.tts.voices().unwrap().iter().map(|v| {
let selected = if let Some(voice) = model.tts.voice().unwrap() {
voice.id() == v.id()
} else {
false
};
option![
attrs! {
At::Value => v.id()
},
if selected {
attrs! {
At::Selected => selected
}
} else {
attrs! {}
},
v.name()
]
}),
input_ev(Ev::Change, Msg::VoiceChanged)
]
]
} else {
div!["Your browser does not seem to support selecting voices."]
},
button![
"Speak",
ev(Ev::Click, |e| {
e.prevent_default();
Msg::Speak
}),
],
]
}
fn main() {
console_log::init().expect("Error initializing logger");
App::start("app", init, update, view);
}

View File

@ -1,18 +1,22 @@
#[cfg(target_os = "android")]
use std::collections::HashSet;
use std::ffi::{CStr, CString};
use std::os::raw::c_void;
use std::sync::{Mutex, RwLock};
use std::thread;
use std::time::Duration;
use std::{
collections::HashSet,
ffi::{CStr, CString},
os::raw::c_void,
sync::{Mutex, RwLock},
thread,
time::{Duration, Instant},
};
use jni::objects::{GlobalRef, JObject, JString};
use jni::sys::{jfloat, jint, JNI_VERSION_1_6};
use jni::{JNIEnv, JavaVM};
use jni::{
objects::{GlobalRef, JObject, JString},
sys::{jfloat, jint, JNI_VERSION_1_6},
JNIEnv, JavaVM,
};
use lazy_static::lazy_static;
use log::{error, info};
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS};
lazy_static! {
static ref BRIDGE: Mutex<Option<GlobalRef>> = Mutex::new(None);
@ -24,7 +28,7 @@ lazy_static! {
#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn JNI_OnLoad(vm: JavaVM, _: *mut c_void) -> jint {
let env = vm.get_env().expect("Cannot get reference to the JNIEnv");
let mut env = vm.get_env().expect("Cannot get reference to the JNIEnv");
let b = env
.find_class("rs/tts/Bridge")
.expect("Failed to find `Bridge`");
@ -38,7 +42,7 @@ pub extern "system" fn JNI_OnLoad(vm: JavaVM, _: *mut c_void) -> jint {
#[no_mangle]
#[allow(non_snake_case)]
pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(env: JNIEnv, obj: JObject, status: jint) {
pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(mut env: JNIEnv, obj: JObject, status: jint) {
let id = env
.get_field(obj, "backendId", "I")
.expect("Failed to get backend ID")
@ -54,7 +58,7 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(env: JNIEnv, obj: JObject, st
#[no_mangle]
#[allow(non_snake_case)]
pub unsafe extern "C" fn Java_rs_tts_Bridge_onStart(
env: JNIEnv,
mut env: JNIEnv,
obj: JObject,
utterance_id: JString,
) {
@ -65,7 +69,7 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onStart(
.expect("Failed to cast to int") as u64;
let backend_id = BackendId::Android(backend_id);
let utterance_id = CString::from(CStr::from_ptr(
env.get_string(utterance_id).unwrap().as_ptr(),
env.get_string(&utterance_id).unwrap().as_ptr(),
))
.into_string()
.unwrap();
@ -81,7 +85,7 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onStart(
#[no_mangle]
#[allow(non_snake_case)]
pub unsafe extern "C" fn Java_rs_tts_Bridge_onStop(
env: JNIEnv,
mut env: JNIEnv,
obj: JObject,
utterance_id: JString,
) {
@ -92,7 +96,7 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onStop(
.expect("Failed to cast to int") as u64;
let backend_id = BackendId::Android(backend_id);
let utterance_id = CString::from(CStr::from_ptr(
env.get_string(utterance_id).unwrap().as_ptr(),
env.get_string(&utterance_id).unwrap().as_ptr(),
))
.into_string()
.unwrap();
@ -108,7 +112,7 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onStop(
#[no_mangle]
#[allow(non_snake_case)]
pub unsafe extern "C" fn Java_rs_tts_Bridge_onDone(
env: JNIEnv,
mut env: JNIEnv,
obj: JObject,
utterance_id: JString,
) {
@ -119,7 +123,7 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onDone(
.expect("Failed to cast to int") as u64;
let backend_id = BackendId::Android(backend_id);
let utterance_id = CString::from(CStr::from_ptr(
env.get_string(utterance_id).unwrap().as_ptr(),
env.get_string(&utterance_id).unwrap().as_ptr(),
))
.into_string()
.unwrap();
@ -135,7 +139,7 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onDone(
#[no_mangle]
#[allow(non_snake_case)]
pub unsafe extern "C" fn Java_rs_tts_Bridge_onError(
env: JNIEnv,
mut env: JNIEnv,
obj: JObject,
utterance_id: JString,
) {
@ -146,7 +150,7 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onError(
.expect("Failed to cast to int") as u64;
let backend_id = BackendId::Android(backend_id);
let utterance_id = CString::from(CStr::from_ptr(
env.get_string(utterance_id).unwrap().as_ptr(),
env.get_string(&utterance_id).unwrap().as_ptr(),
))
.into_string()
.unwrap();
@ -175,22 +179,23 @@ impl Android {
let id = BackendId::Android(bid);
*backend_id += 1;
drop(backend_id);
let native_activity = ndk_glue::native_activity();
let vm = Self::vm()?;
let env = vm.attach_current_thread_permanently()?;
let ctx = ndk_context::android_context();
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }?;
let context = unsafe { JObject::from_raw(ctx.context().cast()) };
let mut env = vm.attach_current_thread_permanently()?;
let bridge = BRIDGE.lock().unwrap();
if let Some(bridge) = &*bridge {
let bridge = env.new_object(bridge, "(I)V", &[(bid as jint).into()])?;
let tts = env.new_object(
"android/speech/tts/TextToSpeech",
"(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V",
&[native_activity.activity().into(), bridge.into()],
&[(&context).into(), (&bridge).into()],
)?;
env.call_method(
tts,
&tts,
"setOnUtteranceProgressListener",
"(Landroid/speech/tts/UtteranceProgressListener;)I",
&[bridge.into()],
&[(&bridge).into()],
)?;
{
let mut pending = PENDING_INITIALIZATIONS.write().unwrap();
@ -198,12 +203,18 @@ impl Android {
}
let tts = env.new_global_ref(tts)?;
// This hack makes my brain bleed.
const MAX_WAIT_TIME: Duration = Duration::from_millis(500);
let start = Instant::now();
// Wait a max of 500ms for initialization, then return an error to avoid hanging.
loop {
{
let pending = PENDING_INITIALIZATIONS.read().unwrap();
if !(*pending).contains(&bid) {
break;
}
if start.elapsed() > MAX_WAIT_TIME {
return Err(Error::OperationFailed);
}
}
thread::sleep(Duration::from_millis(5));
}
@ -219,9 +230,8 @@ impl Android {
}
fn vm() -> Result<JavaVM, jni::errors::Error> {
let native_activity = ndk_glue::native_activity();
let vm_ptr = native_activity.vm();
unsafe { jni::JavaVM::from_raw(vm_ptr) }
let ctx = ndk_context::android_context();
unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }
}
}
@ -238,12 +248,14 @@ impl Backend for Android {
volume: false,
is_speaking: true,
utterance_callbacks: true,
voice: false,
get_voice: false,
}
}
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error> {
let vm = Self::vm()?;
let env = vm.get_env()?;
let mut env = vm.get_env()?;
let tts = self.tts.as_obj();
let text = env.new_string(text)?;
let queue_mode = if interrupt { 0 } else { 1 };
@ -258,10 +270,10 @@ impl Backend for Android {
"speak",
"(Ljava/lang/CharSequence;ILandroid/os/Bundle;Ljava/lang/String;)I",
&[
text.into(),
(&text).into(),
queue_mode.into(),
JObject::null().into(),
uid.into(),
(&JObject::null()).into(),
(&uid).into(),
],
)?;
let rv = rv.i()?;
@ -274,7 +286,7 @@ impl Backend for Android {
fn stop(&mut self) -> Result<(), Error> {
let vm = Self::vm()?;
let env = vm.get_env()?;
let mut env = vm.get_env()?;
let tts = self.tts.as_obj();
let rv = env.call_method(tts, "stop", "()I", &[])?;
let rv = rv.i()?;
@ -303,7 +315,7 @@ impl Backend for Android {
fn set_rate(&mut self, rate: f32) -> Result<(), Error> {
let vm = Self::vm()?;
let env = vm.get_env()?;
let mut env = vm.get_env()?;
let tts = self.tts.as_obj();
let rate = rate as jfloat;
let rv = env.call_method(tts, "setSpeechRate", "(F)I", &[rate.into()])?;
@ -334,7 +346,7 @@ impl Backend for Android {
fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> {
let vm = Self::vm()?;
let env = vm.get_env()?;
let mut env = vm.get_env()?;
let tts = self.tts.as_obj();
let pitch = pitch as jfloat;
let rv = env.call_method(tts, "setPitch", "(F)I", &[pitch.into()])?;
@ -369,10 +381,22 @@ impl Backend for Android {
fn is_speaking(&self) -> Result<bool, Error> {
let vm = Self::vm()?;
let env = vm.get_env()?;
let mut env = vm.get_env()?;
let tts = self.tts.as_obj();
let rv = env.call_method(tts, "isSpeaking", "()Z", &[])?;
let rv = rv.z()?;
Ok(rv)
}
fn voice(&self) -> Result<Option<Voice>, Error> {
unimplemented!()
}
fn voices(&self) -> Result<Vec<Voice>, Error> {
unimplemented!()
}
fn set_voice(&mut self, _voice: &Voice) -> Result<(), Error> {
unimplemented!()
}
}

View File

@ -1,5 +1,4 @@
#[cfg(target_os = "macos")]
#[link(name = "AppKit", kind = "framework")]
use cocoa_foundation::base::{id, nil};
use cocoa_foundation::foundation::NSString;
use log::{info, trace};
@ -7,18 +6,18 @@ use objc::declare::ClassDecl;
use objc::runtime::*;
use objc::*;
use crate::{Backend, BackendId, Error, Features, UtteranceId};
use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice};
#[derive(Clone, Debug)]
pub(crate) struct AppKit(*mut Object, *mut Object);
impl AppKit {
pub(crate) fn new() -> Self {
pub(crate) fn new() -> Result<Self, Error> {
info!("Initializing AppKit backend");
unsafe {
let obj: *mut Object = msg_send![class!(NSSpeechSynthesizer), new];
let mut decl =
ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject)).unwrap();
let mut decl = ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject))
.ok_or(Error::OperationFailed)?;
decl.add_ivar::<id>("synth");
decl.add_ivar::<id>("strings");
@ -47,13 +46,15 @@ impl AppKit {
) {
unsafe {
let strings: id = *this.get_ivar("strings");
let str: id = msg_send!(strings, firstObject);
let _: () = msg_send![str, release];
let _: () = msg_send!(strings, removeObjectAtIndex:0);
let count: u32 = msg_send![strings, count];
if count > 0 {
let str: id = msg_send!(strings, firstObject);
let _: BOOL = msg_send![synth, startSpeakingString: str];
let _: () = msg_send![str, release];
let _: () = msg_send!(strings, removeObjectAtIndex:0);
if count > 1 {
let str: id = msg_send!(strings, firstObject);
let _: BOOL = msg_send![synth, startSpeakingString: str];
}
}
}
}
@ -82,11 +83,17 @@ impl AppKit {
let delegate_class = decl.register();
let delegate_obj: *mut Object = msg_send![delegate_class, new];
delegate_obj.as_mut().unwrap().set_ivar("synth", obj);
delegate_obj
.as_mut()
.ok_or(Error::OperationFailed)?
.set_ivar("synth", obj);
let strings: id = msg_send![class!(NSMutableArray), new];
delegate_obj.as_mut().unwrap().set_ivar("strings", strings);
delegate_obj
.as_mut()
.ok_or(Error::OperationFailed)?
.set_ivar("strings", strings);
let _: Object = msg_send![obj, setDelegate: delegate_obj];
AppKit(obj, delegate_obj)
Ok(AppKit(obj, delegate_obj))
}
}
}
@ -198,7 +205,19 @@ impl Backend for AppKit {
fn is_speaking(&self) -> Result<bool, Error> {
let is_speaking: i8 = unsafe { msg_send![self.0, isSpeaking] };
Ok(is_speaking == YES)
Ok(is_speaking != NO as i8)
}
fn voice(&self) -> Result<Option<Voice>, Error> {
unimplemented!()
}
fn voices(&self) -> Result<Vec<Voice>, Error> {
unimplemented!()
}
fn set_voice(&mut self, _voice: &Voice) -> Result<(), Error> {
unimplemented!()
}
}

View File

@ -1,15 +1,18 @@
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[link(name = "AVFoundation", kind = "framework")]
use std::sync::Mutex;
use cocoa_foundation::base::{id, nil};
use cocoa_foundation::base::{id, nil, NO};
use cocoa_foundation::foundation::NSString;
use core_foundation::array::CFArray;
use core_foundation::base::TCFType;
use core_foundation::string::CFString;
use lazy_static::lazy_static;
use log::{info, trace};
use objc::runtime::{Object, Sel};
use objc::{class, declare::ClassDecl, msg_send, sel, sel_impl};
use oxilangtag::LanguageTag;
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
use crate::{Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, CALLBACKS};
#[derive(Clone, Debug)]
pub(crate) struct AvFoundation {
@ -19,6 +22,7 @@ pub(crate) struct AvFoundation {
rate: f32,
volume: f32,
pitch: f32,
voice: Option<Voice>,
}
lazy_static! {
@ -26,9 +30,10 @@ lazy_static! {
}
impl AvFoundation {
pub(crate) fn new() -> Self {
pub(crate) fn new() -> Result<Self, Error> {
info!("Initializing AVFoundation backend");
let mut decl = ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject)).unwrap();
let mut decl = ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject))
.ok_or(Error::OperationFailed)?;
decl.add_ivar::<u64>("backend_id");
extern "C" fn speech_synthesizer_did_start_speech_utterance(
@ -37,16 +42,22 @@ impl AvFoundation {
_synth: *const Object,
utterance: id,
) {
trace!("speech_synthesizer_did_start_speech_utterance");
unsafe {
let backend_id: u64 = *this.get_ivar("backend_id");
let backend_id = BackendId::AvFoundation(backend_id);
trace!("Locking callbacks");
let mut callbacks = CALLBACKS.lock().unwrap();
trace!("Locked");
let callbacks = callbacks.get_mut(&backend_id).unwrap();
if let Some(callback) = callbacks.utterance_begin.as_mut() {
trace!("Calling utterance_begin");
let utterance_id = UtteranceId::AvFoundation(utterance);
callback(utterance_id);
trace!("Called");
}
}
trace!("Done speech_synthesizer_did_start_speech_utterance");
}
extern "C" fn speech_synthesizer_did_finish_speech_utterance(
@ -55,16 +66,22 @@ impl AvFoundation {
_synth: *const Object,
utterance: id,
) {
trace!("speech_synthesizer_did_finish_speech_utterance");
unsafe {
let backend_id: u64 = *this.get_ivar("backend_id");
let backend_id = BackendId::AvFoundation(backend_id);
trace!("Locking callbacks");
let mut callbacks = CALLBACKS.lock().unwrap();
trace!("Locked");
let callbacks = callbacks.get_mut(&backend_id).unwrap();
if let Some(callback) = callbacks.utterance_end.as_mut() {
trace!("Calling utterance_end");
let utterance_id = UtteranceId::AvFoundation(utterance);
callback(utterance_id);
trace!("Called");
}
}
trace!("Done speech_synthesizer_did_finish_speech_utterance");
}
extern "C" fn speech_synthesizer_did_cancel_speech_utterance(
@ -73,16 +90,22 @@ impl AvFoundation {
_synth: *const Object,
utterance: id,
) {
trace!("speech_synthesizer_did_cancel_speech_utterance");
unsafe {
let backend_id: u64 = *this.get_ivar("backend_id");
let backend_id = BackendId::AvFoundation(backend_id);
trace!("Locking callbacks");
let mut callbacks = CALLBACKS.lock().unwrap();
trace!("Locked");
let callbacks = callbacks.get_mut(&backend_id).unwrap();
if let Some(callback) = callbacks.utterance_stop.as_mut() {
trace!("Calling utterance_stop");
let utterance_id = UtteranceId::AvFoundation(utterance);
callback(utterance_id);
trace!("Called");
}
}
trace!("Done speech_synthesizer_did_cancel_speech_utterance");
}
unsafe {
@ -107,23 +130,28 @@ impl AvFoundation {
let delegate_obj: *mut Object = unsafe { msg_send![delegate_class, new] };
let mut backend_id = NEXT_BACKEND_ID.lock().unwrap();
let rv = unsafe {
trace!("Creating synth");
let synth: *mut Object = msg_send![class!(AVSpeechSynthesizer), new];
trace!("Allocated {:?}", synth);
delegate_obj
.as_mut()
.unwrap()
.set_ivar("backend_id", *backend_id);
trace!("Set backend ID in delegate");
let _: () = msg_send![synth, setDelegate: delegate_obj];
trace!("Assigned delegate: {:?}", delegate_obj);
AvFoundation {
id: BackendId::AvFoundation(*backend_id),
delegate: delegate_obj,
synth: synth,
synth,
rate: 0.5,
volume: 1.,
pitch: 1.,
voice: None,
}
};
*backend_id += 1;
rv
Ok(rv)
}
}
@ -139,24 +167,41 @@ impl Backend for AvFoundation {
pitch: true,
volume: true,
is_speaking: true,
voice: true,
get_voice: false,
utterance_callbacks: true,
}
}
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error> {
trace!("speak({}, {})", text, interrupt);
if interrupt {
if interrupt && self.is_speaking()? {
self.stop()?;
}
let utterance: id;
let mut utterance: id;
unsafe {
let str = NSString::alloc(nil).init_str(text);
trace!("Allocating utterance string");
let mut str = NSString::alloc(nil);
str = str.init_str(text);
trace!("Allocating utterance");
utterance = msg_send![class!(AVSpeechUtterance), alloc];
let _: () = msg_send![utterance, initWithString: str];
trace!("Initializing utterance");
utterance = msg_send![utterance, initWithString: str];
trace!("Setting rate to {}", self.rate);
let _: () = msg_send![utterance, setRate: self.rate];
trace!("Setting volume to {}", self.volume);
let _: () = msg_send![utterance, setVolume: self.volume];
trace!("Setting pitch to {}", self.pitch);
let _: () = msg_send![utterance, setPitchMultiplier: self.pitch];
if let Some(voice) = &self.voice {
let mut vid = NSString::alloc(nil);
vid = vid.init_str(&voice.id());
let v: id = msg_send![class!(AVSpeechSynthesisVoice), voiceWithIdentifier: vid];
let _: () = msg_send![utterance, setVoice: v];
}
trace!("Enqueuing");
let _: () = msg_send![self.synth, speakUtterance: utterance];
trace!("Done queuing");
}
Ok(Some(UtteranceId::AvFoundation(utterance)))
}
@ -208,6 +253,7 @@ impl Backend for AvFoundation {
}
fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> {
trace!("set_pitch({})", pitch);
self.pitch = pitch;
Ok(())
}
@ -229,13 +275,58 @@ impl Backend for AvFoundation {
}
fn set_volume(&mut self, volume: f32) -> Result<(), Error> {
trace!("set_volume({})", volume);
self.volume = volume;
Ok(())
}
fn is_speaking(&self) -> Result<bool, Error> {
trace!("is_speaking()");
let is_speaking: i8 = unsafe { msg_send![self.synth, isSpeaking] };
Ok(is_speaking == 1)
Ok(is_speaking != NO as i8)
}
fn voice(&self) -> Result<Option<Voice>, Error> {
unimplemented!()
}
fn voices(&self) -> Result<Vec<Voice>, Error> {
let voices: CFArray = unsafe {
CFArray::wrap_under_get_rule(msg_send![class!(AVSpeechSynthesisVoice), speechVoices])
};
let rv = voices
.iter()
.map(|v| {
let id: CFString = unsafe {
CFString::wrap_under_get_rule(msg_send![*v as *const Object, identifier])
};
let name: CFString =
unsafe { CFString::wrap_under_get_rule(msg_send![*v as *const Object, name]) };
let gender: i64 = unsafe { msg_send![*v as *const Object, gender] };
let gender = match gender {
1 => Some(Gender::Male),
2 => Some(Gender::Female),
_ => None,
};
let language: CFString = unsafe {
CFString::wrap_under_get_rule(msg_send![*v as *const Object, language])
};
let language = language.to_string();
let language = LanguageTag::parse(language).unwrap();
Voice {
id: id.to_string(),
name: name.to_string(),
gender,
language,
}
})
.collect();
Ok(rv)
}
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> {
self.voice = Some(voice.clone());
Ok(())
}
}

View File

@ -1,27 +1,27 @@
#[cfg(target_os = "linux")]
use std::collections::HashMap;
use std::sync::Mutex;
use std::{collections::HashMap, sync::Mutex};
use lazy_static::*;
use log::{info, trace};
use oxilangtag::LanguageTag;
use speech_dispatcher::*;
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS};
#[derive(Clone, Debug)]
pub(crate) struct SpeechDispatcher(Connection);
lazy_static! {
static ref SPEAKING: Mutex<HashMap<u64, bool>> = {
let m: HashMap<u64, bool> = HashMap::new();
static ref SPEAKING: Mutex<HashMap<usize, bool>> = {
let m: HashMap<usize, bool> = HashMap::new();
Mutex::new(m)
};
}
impl SpeechDispatcher {
pub(crate) fn new() -> Self {
pub(crate) fn new() -> std::result::Result<Self, Error> {
info!("Initializing SpeechDispatcher backend");
let connection = speech_dispatcher::Connection::open("tts", "tts", "tts", Mode::Threaded);
let connection = speech_dispatcher::Connection::open("tts", "tts", "tts", Mode::Threaded)?;
let sd = SpeechDispatcher(connection);
let mut speaking = SPEAKING.lock().unwrap();
speaking.insert(sd.0.client_id(), false);
@ -31,7 +31,7 @@ impl SpeechDispatcher {
let mut callbacks = CALLBACKS.lock().unwrap();
let backend_id = BackendId::SpeechDispatcher(client_id);
let cb = callbacks.get_mut(&backend_id).unwrap();
let utterance_id = UtteranceId::SpeechDispatcher(msg_id);
let utterance_id = UtteranceId::SpeechDispatcher(msg_id as u64);
if let Some(f) = cb.utterance_begin.as_mut() {
f(utterance_id);
}
@ -42,7 +42,7 @@ impl SpeechDispatcher {
let mut callbacks = CALLBACKS.lock().unwrap();
let backend_id = BackendId::SpeechDispatcher(client_id);
let cb = callbacks.get_mut(&backend_id).unwrap();
let utterance_id = UtteranceId::SpeechDispatcher(msg_id);
let utterance_id = UtteranceId::SpeechDispatcher(msg_id as u64);
if let Some(f) = cb.utterance_end.as_mut() {
f(utterance_id);
}
@ -53,7 +53,7 @@ impl SpeechDispatcher {
let mut callbacks = CALLBACKS.lock().unwrap();
let backend_id = BackendId::SpeechDispatcher(client_id);
let cb = callbacks.get_mut(&backend_id).unwrap();
let utterance_id = UtteranceId::SpeechDispatcher(msg_id);
let utterance_id = UtteranceId::SpeechDispatcher(msg_id as u64);
if let Some(f) = cb.utterance_stop.as_mut() {
f(utterance_id);
}
@ -66,7 +66,7 @@ impl SpeechDispatcher {
let mut speaking = SPEAKING.lock().unwrap();
speaking.insert(client_id, true);
})));
sd
Ok(sd)
}
}
@ -82,6 +82,8 @@ impl Backend for SpeechDispatcher {
pitch: true,
volume: true,
is_speaking: true,
voice: true,
get_voice: false,
utterance_callbacks: true,
}
}
@ -93,11 +95,11 @@ impl Backend for SpeechDispatcher {
}
let single_char = text.to_string().capacity() == 1;
if single_char {
self.0.set_punctuation(Punctuation::All);
self.0.set_punctuation(Punctuation::All)?;
}
let id = self.0.say(Priority::Important, text);
if single_char {
self.0.set_punctuation(Punctuation::None);
self.0.set_punctuation(Punctuation::None)?;
}
if let Some(id) = id {
Ok(Some(UtteranceId::SpeechDispatcher(id)))
@ -108,7 +110,7 @@ impl Backend for SpeechDispatcher {
fn stop(&mut self) -> Result<(), Error> {
trace!("stop()");
self.0.cancel();
self.0.cancel()?;
Ok(())
}
@ -129,7 +131,7 @@ impl Backend for SpeechDispatcher {
}
fn set_rate(&mut self, rate: f32) -> Result<(), Error> {
self.0.set_voice_rate(rate as i32);
self.0.set_voice_rate(rate as i32)?;
Ok(())
}
@ -150,7 +152,7 @@ impl Backend for SpeechDispatcher {
}
fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> {
self.0.set_voice_pitch(pitch as i32);
self.0.set_voice_pitch(pitch as i32)?;
Ok(())
}
@ -171,7 +173,7 @@ impl Backend for SpeechDispatcher {
}
fn set_volume(&mut self, volume: f32) -> Result<(), Error> {
self.0.set_volume(volume as i32);
self.0.set_volume(volume as i32)?;
Ok(())
}
@ -180,6 +182,36 @@ impl Backend for SpeechDispatcher {
let is_speaking = speaking.get(&self.0.client_id()).unwrap();
Ok(*is_speaking)
}
fn voices(&self) -> Result<Vec<Voice>, Error> {
let rv = self
.0
.list_synthesis_voices()?
.iter()
.filter(|v| LanguageTag::parse(v.language.clone()).is_ok())
.map(|v| Voice {
id: v.name.clone(),
name: v.name.clone(),
gender: None,
language: LanguageTag::parse(v.language.clone()).unwrap(),
})
.collect::<Vec<Voice>>();
Ok(rv)
}
fn voice(&self) -> Result<Option<Voice>, Error> {
unimplemented!()
}
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> {
for v in self.0.list_synthesis_voices()? {
if v.name == voice.name {
self.0.set_synthesis_voice(&v)?;
return Ok(());
}
}
Err(Error::OperationFailed)
}
}
impl Drop for SpeechDispatcher {

View File

@ -1,11 +1,13 @@
#[cfg(all(windows, feature = "tolk"))]
use std::sync::Arc;
use log::{info, trace};
use tolk::Tolk as TolkPtr;
use crate::{Backend, BackendId, Error, Features, UtteranceId};
use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice};
#[derive(Clone, Debug)]
pub(crate) struct Tolk(TolkPtr);
pub(crate) struct Tolk(Arc<TolkPtr>);
impl Tolk {
pub(crate) fn new() -> Option<Self> {
@ -106,4 +108,16 @@ impl Backend for Tolk {
fn is_speaking(&self) -> Result<bool, Error> {
unimplemented!()
}
fn voice(&self) -> Result<Option<Voice>, Error> {
unimplemented!()
}
fn voices(&self) -> Result<Vec<Voice>, Error> {
unimplemented!()
}
fn set_voice(&mut self, _voice: &Voice) -> Result<(), Error> {
unimplemented!()
}
}

View File

@ -3,14 +3,15 @@ use std::sync::Mutex;
use lazy_static::lazy_static;
use log::{info, trace};
use oxilangtag::LanguageTag;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{
SpeechSynthesisErrorCode, SpeechSynthesisErrorEvent, SpeechSynthesisEvent,
SpeechSynthesisUtterance,
SpeechSynthesisUtterance, SpeechSynthesisVoice,
};
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS};
#[derive(Clone, Debug)]
pub struct Web {
@ -18,6 +19,7 @@ pub struct Web {
rate: f32,
pitch: f32,
volume: f32,
voice: Option<SpeechSynthesisVoice>,
}
lazy_static! {
@ -35,6 +37,7 @@ impl Web {
rate: 1.,
pitch: 1.,
volume: 1.,
voice: None,
};
*backend_id += 1;
Ok(rv)
@ -53,6 +56,8 @@ impl Backend for Web {
pitch: true,
volume: true,
is_speaking: true,
voice: true,
get_voice: true,
utterance_callbacks: true,
}
}
@ -63,6 +68,9 @@ impl Backend for Web {
utterance.set_rate(self.rate);
utterance.set_pitch(self.pitch);
utterance.set_volume(self.volume);
if self.voice.is_some() {
utterance.set_voice(self.voice.as_ref());
}
let id = self.id().unwrap();
let mut uid = NEXT_UTTERANCE_ID.lock().unwrap();
let utterance_id = UtteranceId::Web(*uid);
@ -196,6 +204,55 @@ impl Backend for Web {
Err(Error::NoneError)
}
}
fn voice(&self) -> Result<Option<Voice>, Error> {
if let Some(voice) = &self.voice {
Ok(Some(voice.clone().into()))
} else {
if let Some(window) = web_sys::window() {
let speech_synthesis = window.speech_synthesis().unwrap();
for voice in speech_synthesis.get_voices().iter() {
let voice: SpeechSynthesisVoice = voice.into();
if voice.default() {
return Ok(Some(voice.into()));
}
}
} else {
return Err(Error::NoneError);
}
Ok(None)
}
}
fn voices(&self) -> Result<Vec<Voice>, Error> {
if let Some(window) = web_sys::window() {
let speech_synthesis = window.speech_synthesis().unwrap();
let mut rv: Vec<Voice> = vec![];
for v in speech_synthesis.get_voices().iter() {
let v: SpeechSynthesisVoice = v.into();
rv.push(v.into());
}
Ok(rv)
} else {
Err(Error::NoneError)
}
}
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> {
if let Some(window) = web_sys::window() {
let speech_synthesis = window.speech_synthesis().unwrap();
for v in speech_synthesis.get_voices().iter() {
let v: SpeechSynthesisVoice = v.into();
if v.voice_uri() == voice.id {
self.voice = Some(v);
return Ok(());
}
}
Err(Error::OperationFailed)
} else {
Err(Error::NoneError)
}
}
}
impl Drop for Web {
@ -204,3 +261,15 @@ impl Drop for Web {
mappings.retain(|v| v.0 != self.id);
}
}
impl From<SpeechSynthesisVoice> for Voice {
fn from(other: SpeechSynthesisVoice) -> Self {
let language = LanguageTag::parse(other.lang()).unwrap();
Voice {
id: other.voice_uri(),
name: other.name(),
gender: None,
language,
}
}
}

View File

@ -1,37 +1,38 @@
#[cfg(windows)]
use std::collections::{HashMap, VecDeque};
use std::sync::Mutex;
use std::{
collections::{HashMap, VecDeque},
sync::Mutex,
};
use lazy_static::lazy_static;
use log::{info, trace};
mod bindings;
use bindings::windows::{
foundation::TypedEventHandler,
media::{
core::MediaSource,
playback::{MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory},
speech_synthesis::SpeechSynthesizer,
use oxilangtag::LanguageTag;
use windows::{
Foundation::TypedEventHandler,
Media::{
Core::MediaSource,
Playback::{MediaPlayer, MediaPlayerAudioCategory},
SpeechSynthesis::{SpeechSynthesizer, VoiceGender, VoiceInformation},
},
};
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
use crate::{Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, CALLBACKS};
impl From<windows::Error> for Error {
fn from(e: windows::Error) -> Self {
Error::WinRT(e)
impl From<windows::core::Error> for Error {
fn from(e: windows::core::Error) -> Self {
Error::WinRt(e)
}
}
#[derive(Clone, Debug)]
pub struct WinRT {
#[derive(Clone)]
pub struct WinRt {
id: BackendId,
synth: SpeechSynthesizer,
player: MediaPlayer,
rate: f32,
pitch: f32,
volume: f32,
voice: VoiceInformation,
}
struct Utterance {
@ -40,6 +41,7 @@ struct Utterance {
rate: f32,
pitch: f32,
volume: f32,
voice: VoiceInformation,
}
lazy_static! {
@ -59,15 +61,15 @@ lazy_static! {
};
}
impl WinRT {
impl WinRt {
pub fn new() -> std::result::Result<Self, Error> {
info!("Initializing WinRT backend");
let synth = SpeechSynthesizer::new()?;
let player = MediaPlayer::new()?;
player.set_real_time_playback(true)?;
player.set_audio_category(MediaPlayerAudioCategory::Speech)?;
player.SetRealTimePlayback(true)?;
player.SetAudioCategory(MediaPlayerAudioCategory::Speech)?;
let mut backend_id = NEXT_BACKEND_ID.lock().unwrap();
let bid = BackendId::WinRT(*backend_id);
let bid = BackendId::WinRt(*backend_id);
*backend_id += 1;
drop(backend_id);
{
@ -81,7 +83,7 @@ impl WinRT {
backend_to_speech_synthesizer.insert(bid, synth.clone());
drop(backend_to_speech_synthesizer);
let bid_clone = bid;
player.media_ended(TypedEventHandler::new(
player.MediaEnded(&TypedEventHandler::new(
move |sender: &Option<MediaPlayer>, _args| {
if let Some(sender) = sender {
let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap();
@ -102,19 +104,18 @@ impl WinRT {
.iter()
.find(|v| *v.0 == bid_clone);
if let Some((_, tts)) = id {
tts.options()?.set_speaking_rate(utterance.rate.into())?;
tts.options()?.set_audio_pitch(utterance.pitch.into())?;
tts.options()?.set_audio_volume(utterance.volume.into())?;
let stream = tts
.synthesize_text_to_stream_async(
utterance.text.as_str(),
)?
.get()?;
let content_type = stream.content_type()?;
tts.Options()?.SetSpeakingRate(utterance.rate.into())?;
tts.Options()?.SetAudioPitch(utterance.pitch.into())?;
tts.Options()?.SetAudioVolume(utterance.volume.into())?;
tts.SetVoice(&utterance.voice)?;
let text = &utterance.text;
let stream =
tts.SynthesizeTextToStreamAsync(&text.into())?.get()?;
let content_type = stream.ContentType()?;
let source =
MediaSource::create_from_stream(stream, content_type)?;
sender.set_source(source)?;
sender.play()?;
MediaSource::CreateFromStream(&stream, &content_type)?;
sender.SetSource(&source)?;
sender.Play()?;
if let Some(callback) = callbacks.utterance_begin.as_mut() {
callback(utterance.id);
}
@ -134,11 +135,12 @@ impl WinRT {
rate: 1.,
pitch: 1.,
volume: 1.,
voice: SpeechSynthesizer::DefaultVoice()?,
})
}
}
impl Backend for WinRT {
impl Backend for WinRt {
fn id(&self) -> Option<BackendId> {
Some(self.id)
}
@ -150,6 +152,8 @@ impl Backend for WinRT {
pitch: true,
volume: true,
is_speaking: true,
voice: true,
get_voice: true,
utterance_callbacks: true,
}
}
@ -164,7 +168,7 @@ impl Backend for WinRT {
}
let utterance_id = {
let mut uid = NEXT_UTTERANCE_ID.lock().unwrap();
let utterance_id = UtteranceId::WinRT(*uid);
let utterance_id = UtteranceId::WinRt(*uid);
*uid += 1;
utterance_id
};
@ -179,21 +183,24 @@ impl Backend for WinRT {
rate: self.rate,
pitch: self.pitch,
volume: self.volume,
voice: self.voice.clone(),
};
utterances.push_back(utterance);
}
}
if no_utterances
&& self.player.playback_session()?.playback_state()? != MediaPlaybackState::Playing
{
self.synth.options()?.set_speaking_rate(self.rate.into())?;
self.synth.options()?.set_audio_pitch(self.pitch.into())?;
self.synth.options()?.set_audio_volume(self.volume.into())?;
let stream = self.synth.synthesize_text_to_stream_async(text)?.get()?;
let content_type = stream.content_type()?;
let source = MediaSource::create_from_stream(stream, content_type)?;
self.player.set_source(source)?;
self.player.play()?;
if no_utterances {
self.synth.Options()?.SetSpeakingRate(self.rate.into())?;
self.synth.Options()?.SetAudioPitch(self.pitch.into())?;
self.synth.Options()?.SetAudioVolume(self.volume.into())?;
self.synth.SetVoice(&self.voice)?;
let stream = self
.synth
.SynthesizeTextToStreamAsync(&text.into())?
.get()?;
let content_type = stream.ContentType()?;
let source = MediaSource::CreateFromStream(&stream, &content_type)?;
self.player.SetSource(&source)?;
self.player.Play()?;
let mut callbacks = CALLBACKS.lock().unwrap();
let callbacks = callbacks.get_mut(&self.id).unwrap();
if let Some(callback) = callbacks.utterance_begin.as_mut() {
@ -221,7 +228,7 @@ impl Backend for WinRT {
if let Some(utterances) = utterances.get_mut(&self.id) {
utterances.clear();
}
self.player.pause()?;
self.player.Pause()?;
Ok(())
}
@ -238,7 +245,7 @@ impl Backend for WinRT {
}
fn get_rate(&self) -> std::result::Result<f32, Error> {
let rate = self.synth.options()?.speaking_rate()?;
let rate = self.synth.Options()?.SpeakingRate()?;
Ok(rate as f32)
}
@ -260,7 +267,7 @@ impl Backend for WinRT {
}
fn get_pitch(&self) -> std::result::Result<f32, Error> {
let pitch = self.synth.options()?.audio_pitch()?;
let pitch = self.synth.Options()?.AudioPitch()?;
Ok(pitch as f32)
}
@ -282,7 +289,7 @@ impl Backend for WinRT {
}
fn get_volume(&self) -> std::result::Result<f32, Error> {
let volume = self.synth.options()?.audio_volume()?;
let volume = self.synth.Options()?.AudioVolume()?;
Ok(volume as f32)
}
@ -296,9 +303,34 @@ impl Backend for WinRT {
let utterances = utterances.get(&self.id).unwrap();
Ok(!utterances.is_empty())
}
fn voice(&self) -> Result<Option<Voice>, Error> {
let voice = self.synth.Voice()?;
let voice = voice.try_into()?;
Ok(Some(voice))
}
fn voices(&self) -> Result<Vec<Voice>, Error> {
let mut rv: Vec<Voice> = vec![];
for voice in SpeechSynthesizer::AllVoices()? {
rv.push(voice.try_into()?);
}
Ok(rv)
}
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> {
for v in SpeechSynthesizer::AllVoices()? {
let vid: String = v.Id()?.try_into()?;
if vid == voice.id {
self.voice = v;
return Ok(());
}
}
Err(Error::OperationFailed)
}
}
impl Drop for WinRT {
impl Drop for WinRt {
fn drop(&mut self) {
let id = self.id;
let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap();
@ -309,3 +341,24 @@ impl Drop for WinRT {
utterances.remove(&id);
}
}
impl TryInto<Voice> for VoiceInformation {
type Error = Error;
fn try_into(self) -> Result<Voice, Self::Error> {
let gender = self.Gender()?;
let gender = if gender == VoiceGender::Male {
Gender::Male
} else {
Gender::Female
};
let language: String = self.Language()?.try_into()?;
let language = LanguageTag::parse(language).unwrap();
Ok(Voice {
id: self.Id()?.try_into()?,
name: self.DisplayName()?.try_into()?,
gender: Some(gender),
language,
})
}
}

View File

@ -1 +0,0 @@
::windows::include_bindings!();

View File

@ -1,22 +1,24 @@
/*!
* a Text-To-Speech (TTS) library providing high-level interfaces to a variety of backends.
* Currently supported backends are:
* * Windows
* * Screen readers/SAPI via Tolk (requires `tolk` Cargo feature)
* * WinRT
* * Linux via [Speech Dispatcher](https://freebsoft.org/speechd)
* * MacOS/iOS
* * AppKit on MacOS 10.13 and below
* * AVFoundation on MacOS 10.14 and above, and iOS
* * Android
* * WebAssembly
*/
//! * a Text-To-Speech (TTS) library providing high-level interfaces to a variety of backends.
//! * Currently supported backends are:
//! * * Windows
//! * * Screen readers/SAPI via Tolk (requires `tolk` Cargo feature)
//! * * WinRT
//! * * Linux via [Speech Dispatcher](https://freebsoft.org/speechd)
//! * * MacOS/iOS
//! * * AppKit on MacOS 10.13 and below
//! * * AVFoundation on MacOS 10.14 and above, and iOS
//! * * Android
//! * * WebAssembly
use std::boxed::Box;
use std::collections::HashMap;
#[cfg(target_os = "macos")]
use std::ffi::CStr;
use std::fmt;
use std::rc::Rc;
#[cfg(windows)]
use std::string::FromUtf16Error;
use std::sync::Mutex;
use std::{boxed::Box, sync::RwLock};
#[cfg(any(target_os = "macos", target_os = "ios"))]
use cocoa_foundation::base::id;
@ -26,96 +28,182 @@ use lazy_static::lazy_static;
use libc::c_char;
#[cfg(target_os = "macos")]
use objc::{class, msg_send, sel, sel_impl};
pub use oxilangtag::LanguageTag;
#[cfg(target_os = "linux")]
use speech_dispatcher::Error as SpeechDispatcherError;
use thiserror::Error;
#[cfg(all(windows, feature = "tolk"))]
use tolk::Tolk;
mod backends;
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Backends {
#[cfg(target_os = "linux")]
SpeechDispatcher,
#[cfg(target_arch = "wasm32")]
Web,
#[cfg(all(windows, feature = "tolk"))]
Tolk,
#[cfg(windows)]
WinRT,
#[cfg(target_os = "android")]
Android,
#[cfg(target_os = "macos")]
AppKit,
#[cfg(any(target_os = "macos", target_os = "ios"))]
AvFoundation,
#[cfg(target_os = "android")]
Android,
#[cfg(target_os = "linux")]
SpeechDispatcher,
#[cfg(all(windows, feature = "tolk"))]
Tolk,
#[cfg(target_arch = "wasm32")]
Web,
#[cfg(windows)]
WinRt,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
impl fmt::Display for Backends {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
#[cfg(target_os = "android")]
Backends::Android => writeln!(f, "Android"),
#[cfg(target_os = "macos")]
Backends::AppKit => writeln!(f, "AppKit"),
#[cfg(any(target_os = "macos", target_os = "ios"))]
Backends::AvFoundation => writeln!(f, "AVFoundation"),
#[cfg(target_os = "linux")]
Backends::SpeechDispatcher => writeln!(f, "Speech Dispatcher"),
#[cfg(all(windows, feature = "tolk"))]
Backends::Tolk => writeln!(f, "Tolk"),
#[cfg(target_arch = "wasm32")]
Backends::Web => writeln!(f, "Web"),
#[cfg(windows)]
Backends::WinRt => writeln!(f, "Windows Runtime"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BackendId {
#[cfg(target_os = "linux")]
SpeechDispatcher(u64),
#[cfg(target_arch = "wasm32")]
Web(u64),
#[cfg(windows)]
WinRT(u64),
#[cfg(target_os = "android")]
Android(u64),
#[cfg(any(target_os = "macos", target_os = "ios"))]
AvFoundation(u64),
#[cfg(target_os = "android")]
Android(u64),
#[cfg(target_os = "linux")]
SpeechDispatcher(usize),
#[cfg(target_arch = "wasm32")]
Web(u64),
#[cfg(windows)]
WinRt(u64),
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
impl fmt::Display for BackendId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
#[cfg(target_os = "android")]
BackendId::Android(id) => writeln!(f, "Android({id})"),
#[cfg(any(target_os = "macos", target_os = "ios"))]
BackendId::AvFoundation(id) => writeln!(f, "AvFoundation({id})"),
#[cfg(target_os = "linux")]
BackendId::SpeechDispatcher(id) => writeln!(f, "SpeechDispatcher({id})"),
#[cfg(target_arch = "wasm32")]
BackendId::Web(id) => writeln!(f, "Web({id})"),
#[cfg(windows)]
BackendId::WinRt(id) => writeln!(f, "WinRT({id})"),
}
}
}
// # Note
//
// Most trait implementations are blocked by cocoa_foundation::base::id;
// which is a type alias for objc::runtime::Object, which only implements Debug.
#[derive(Debug)]
#[cfg_attr(
not(any(target_os = "macos", target_os = "ios")),
derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)
)]
#[cfg_attr(
all(feature = "serde", not(any(target_os = "macos", target_os = "ios"))),
derive(serde::Serialize, serde::Deserialize)
)]
pub enum UtteranceId {
#[cfg(target_os = "android")]
Android(u64),
#[cfg(any(target_os = "macos", target_os = "ios"))]
AvFoundation(id),
#[cfg(target_os = "linux")]
SpeechDispatcher(u64),
#[cfg(target_arch = "wasm32")]
Web(u64),
#[cfg(windows)]
WinRT(u64),
#[cfg(any(target_os = "macos", target_os = "ios"))]
AvFoundation(id),
#[cfg(target_os = "android")]
Android(u64),
WinRt(u64),
}
// # Note
//
// Display is not implemented by cocoa_foundation::base::id;
// which is a type alias for objc::runtime::Object, which only implements Debug.
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
impl fmt::Display for UtteranceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
#[cfg(target_os = "android")]
UtteranceId::Android(id) => writeln!(f, "Android({id})"),
#[cfg(target_os = "linux")]
UtteranceId::SpeechDispatcher(id) => writeln!(f, "SpeechDispatcher({id})"),
#[cfg(target_arch = "wasm32")]
UtteranceId::Web(id) => writeln!(f, "Web({})", id),
#[cfg(windows)]
UtteranceId::WinRt(id) => writeln!(f, "WinRt({id})"),
}
}
}
unsafe impl Send for UtteranceId {}
unsafe impl Sync for UtteranceId {}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Features {
pub stop: bool,
pub rate: bool,
pub pitch: bool,
pub volume: bool,
pub is_speaking: bool,
pub pitch: bool,
pub rate: bool,
pub stop: bool,
pub utterance_callbacks: bool,
pub voice: bool,
pub get_voice: bool,
pub volume: bool,
}
impl Default for Features {
fn default() -> Self {
Self {
stop: false,
rate: false,
pitch: false,
volume: false,
is_speaking: false,
utterance_callbacks: false,
}
impl fmt::Display for Features {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
writeln!(f, "{self:#?}")
}
}
impl Features {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error("IO error: {0}")]
IO(#[from] std::io::Error),
Io(#[from] std::io::Error),
#[error("Value not received")]
NoneError,
#[error("Operation failed")]
OperationFailed,
#[cfg(target_arch = "wasm32")]
#[error("JavaScript error: [0])]")]
#[error("JavaScript error: [0]")]
JavaScriptError(wasm_bindgen::JsValue),
#[cfg(target_os = "linux")]
#[error("Speech Dispatcher error: {0}")]
SpeechDispatcher(#[from] SpeechDispatcherError),
#[cfg(windows)]
#[error("WinRT error")]
WinRT(windows::Error),
WinRt(windows::core::Error),
#[cfg(windows)]
#[error("UTF string conversion failed")]
UtfStringConversionFailed(#[from] FromUtf16Error),
#[error("Unsupported feature")]
UnsupportedFeature,
#[error("Out of range")]
@ -126,7 +214,7 @@ pub enum Error {
}
#[clonable]
trait Backend: Clone {
pub trait Backend: Clone {
fn id(&self) -> Option<BackendId>;
fn supported_features(&self) -> Features;
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error>;
@ -147,6 +235,9 @@ trait Backend: Clone {
fn get_volume(&self) -> Result<f32, Error>;
fn set_volume(&mut self, volume: f32) -> Result<(), Error>;
fn is_speaking(&self) -> Result<bool, Error>;
fn voices(&self) -> Result<Vec<Voice>, Error>;
fn voice(&self) -> Result<Option<Voice>, Error>;
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error>;
}
#[derive(Default)]
@ -168,51 +259,56 @@ lazy_static! {
}
#[derive(Clone)]
pub struct TTS(Box<dyn Backend>);
pub struct Tts(Rc<RwLock<Box<dyn Backend>>>);
unsafe impl Send for TTS {}
unsafe impl Send for Tts {}
unsafe impl Sync for TTS {}
unsafe impl Sync for Tts {}
impl TTS {
/**
* Create a new `TTS` instance with the specified backend.
*/
pub fn new(backend: Backends) -> Result<TTS, Error> {
impl Tts {
/// Create a new `TTS` instance with the specified backend.
pub fn new(backend: Backends) -> Result<Tts, Error> {
let backend = match backend {
#[cfg(target_os = "linux")]
Backends::SpeechDispatcher => Ok(TTS(Box::new(backends::SpeechDispatcher::new()))),
Backends::SpeechDispatcher => {
let tts = backends::SpeechDispatcher::new()?;
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
}
#[cfg(target_arch = "wasm32")]
Backends::Web => {
let tts = backends::Web::new()?;
Ok(TTS(Box::new(tts)))
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
}
#[cfg(all(windows, feature = "tolk"))]
Backends::Tolk => {
let tts = backends::Tolk::new();
if let Some(tts) = tts {
Ok(TTS(Box::new(tts)))
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
} else {
Err(Error::NoneError)
}
}
#[cfg(windows)]
Backends::WinRT => {
let tts = backends::WinRT::new()?;
Ok(TTS(Box::new(tts)))
Backends::WinRt => {
let tts = backends::WinRt::new()?;
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
}
#[cfg(target_os = "macos")]
Backends::AppKit => Ok(TTS(Box::new(backends::AppKit::new()))),
Backends::AppKit => Ok(Tts(Rc::new(RwLock::new(
Box::new(backends::AppKit::new()?),
)))),
#[cfg(any(target_os = "macos", target_os = "ios"))]
Backends::AvFoundation => Ok(TTS(Box::new(backends::AvFoundation::new()))),
Backends::AvFoundation => Ok(Tts(Rc::new(RwLock::new(Box::new(
backends::AvFoundation::new()?,
))))),
#[cfg(target_os = "android")]
Backends::Android => {
let tts = backends::Android::new()?;
Ok(TTS(Box::new(tts)))
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
}
};
if let Ok(backend) = backend {
if let Some(id) = backend.0.id() {
if let Some(id) = backend.0.read().unwrap().id() {
let mut callbacks = CALLBACKS.lock().unwrap();
callbacks.insert(id, Callbacks::default());
}
@ -222,19 +318,20 @@ impl TTS {
}
}
pub fn default() -> Result<TTS, Error> {
#[allow(clippy::should_implement_trait)]
pub fn default() -> Result<Tts, Error> {
#[cfg(target_os = "linux")]
let tts = TTS::new(Backends::SpeechDispatcher);
let tts = Tts::new(Backends::SpeechDispatcher);
#[cfg(all(windows, feature = "tolk"))]
let tts = if let Ok(tts) = TTS::new(Backends::Tolk) {
let tts = if let Ok(tts) = Tts::new(Backends::Tolk) {
Ok(tts)
} else {
TTS::new(Backends::WinRT)
Tts::new(Backends::WinRt)
};
#[cfg(all(windows, not(feature = "tolk")))]
let tts = TTS::new(Backends::WinRT);
let tts = Tts::new(Backends::WinRt);
#[cfg(target_arch = "wasm32")]
let tts = TTS::new(Backends::Web);
let tts = Tts::new(Backends::Web);
#[cfg(target_os = "macos")]
let tts = unsafe {
// Needed because the Rust NSProcessInfo structs report bogus values, and I don't want to pull in a full bindgen stack.
@ -243,100 +340,88 @@ impl TTS {
let str: *const c_char = msg_send![version, UTF8String];
let str = CStr::from_ptr(str);
let str = str.to_string_lossy();
let version: Vec<&str> = str.split(" ").collect();
let version: Vec<&str> = str.split(' ').collect();
let version = version[1];
let version_parts: Vec<&str> = version.split(".").collect();
let version_parts: Vec<&str> = version.split('.').collect();
let major_version: i8 = version_parts[0].parse().unwrap();
let minor_version: i8 = version_parts[1].parse().unwrap();
if major_version >= 11 || minor_version >= 14 {
TTS::new(Backends::AvFoundation)
Tts::new(Backends::AvFoundation)
} else {
TTS::new(Backends::AppKit)
Tts::new(Backends::AppKit)
}
};
#[cfg(target_os = "ios")]
let tts = TTS::new(Backends::AvFoundation);
let tts = Tts::new(Backends::AvFoundation);
#[cfg(target_os = "android")]
let tts = TTS::new(Backends::Android);
let tts = Tts::new(Backends::Android);
tts
}
/**
* Returns the features supported by this TTS engine
*/
/// Returns the features supported by this TTS engine
pub fn supported_features(&self) -> Features {
self.0.supported_features()
self.0.read().unwrap().supported_features()
}
/**
* Speaks the specified text, optionally interrupting current speech.
*/
/// Speaks the specified text, optionally interrupting current speech.
pub fn speak<S: Into<String>>(
&mut self,
text: S,
interrupt: bool,
) -> Result<Option<UtteranceId>, Error> {
self.0.speak(text.into().as_str(), interrupt)
self.0
.write()
.unwrap()
.speak(text.into().as_str(), interrupt)
}
/**
* Stops current speech.
*/
/// Stops current speech.
pub fn stop(&mut self) -> Result<&Self, Error> {
let Features { stop, .. } = self.supported_features();
if stop {
self.0.stop()?;
self.0.write().unwrap().stop()?;
Ok(self)
} else {
Err(Error::UnsupportedFeature)
}
}
/**
* Returns the minimum rate for this speech synthesizer.
*/
/// Returns the minimum rate for this speech synthesizer.
pub fn min_rate(&self) -> f32 {
self.0.min_rate()
self.0.read().unwrap().min_rate()
}
/**
* Returns the maximum rate for this speech synthesizer.
*/
/// Returns the maximum rate for this speech synthesizer.
pub fn max_rate(&self) -> f32 {
self.0.max_rate()
self.0.read().unwrap().max_rate()
}
/**
* Returns the normal rate for this speech synthesizer.
*/
/// Returns the normal rate for this speech synthesizer.
pub fn normal_rate(&self) -> f32 {
self.0.normal_rate()
self.0.read().unwrap().normal_rate()
}
/**
* Gets the current speech rate.
*/
/// Gets the current speech rate.
pub fn get_rate(&self) -> Result<f32, Error> {
let Features { rate, .. } = self.supported_features();
if rate {
self.0.get_rate()
self.0.read().unwrap().get_rate()
} else {
Err(Error::UnsupportedFeature)
}
}
/**
* Sets the desired speech rate.
*/
/// Sets the desired speech rate.
pub fn set_rate(&mut self, rate: f32) -> Result<&Self, Error> {
let Features {
rate: rate_feature, ..
} = self.supported_features();
if rate_feature {
if rate < self.0.min_rate() || rate > self.0.max_rate() {
let mut backend = self.0.write().unwrap();
if rate < backend.min_rate() || rate > backend.max_rate() {
Err(Error::OutOfRange)
} else {
self.0.set_rate(rate)?;
backend.set_rate(rate)?;
Ok(self)
}
} else {
@ -344,52 +429,43 @@ impl TTS {
}
}
/**
* Returns the minimum pitch for this speech synthesizer.
*/
/// Returns the minimum pitch for this speech synthesizer.
pub fn min_pitch(&self) -> f32 {
self.0.min_pitch()
self.0.read().unwrap().min_pitch()
}
/**
* Returns the maximum pitch for this speech synthesizer.
*/
/// Returns the maximum pitch for this speech synthesizer.
pub fn max_pitch(&self) -> f32 {
self.0.max_pitch()
self.0.read().unwrap().max_pitch()
}
/**
* Returns the normal pitch for this speech synthesizer.
*/
/// Returns the normal pitch for this speech synthesizer.
pub fn normal_pitch(&self) -> f32 {
self.0.normal_pitch()
self.0.read().unwrap().normal_pitch()
}
/**
* Gets the current speech pitch.
*/
/// Gets the current speech pitch.
pub fn get_pitch(&self) -> Result<f32, Error> {
let Features { pitch, .. } = self.supported_features();
if pitch {
self.0.get_pitch()
self.0.read().unwrap().get_pitch()
} else {
Err(Error::UnsupportedFeature)
}
}
/**
* Sets the desired speech pitch.
*/
/// Sets the desired speech pitch.
pub fn set_pitch(&mut self, pitch: f32) -> Result<&Self, Error> {
let Features {
pitch: pitch_feature,
..
} = self.supported_features();
if pitch_feature {
if pitch < self.0.min_pitch() || pitch > self.0.max_pitch() {
let mut backend = self.0.write().unwrap();
if pitch < backend.min_pitch() || pitch > backend.max_pitch() {
Err(Error::OutOfRange)
} else {
self.0.set_pitch(pitch)?;
backend.set_pitch(pitch)?;
Ok(self)
}
} else {
@ -397,52 +473,43 @@ impl TTS {
}
}
/**
* Returns the minimum volume for this speech synthesizer.
*/
/// Returns the minimum volume for this speech synthesizer.
pub fn min_volume(&self) -> f32 {
self.0.min_volume()
self.0.read().unwrap().min_volume()
}
/**
* Returns the maximum volume for this speech synthesizer.
*/
/// Returns the maximum volume for this speech synthesizer.
pub fn max_volume(&self) -> f32 {
self.0.max_volume()
self.0.read().unwrap().max_volume()
}
/**
* Returns the normal volume for this speech synthesizer.
*/
/// Returns the normal volume for this speech synthesizer.
pub fn normal_volume(&self) -> f32 {
self.0.normal_volume()
self.0.read().unwrap().normal_volume()
}
/**
* Gets the current speech volume.
*/
/// Gets the current speech volume.
pub fn get_volume(&self) -> Result<f32, Error> {
let Features { volume, .. } = self.supported_features();
if volume {
self.0.get_volume()
self.0.read().unwrap().get_volume()
} else {
Err(Error::UnsupportedFeature)
}
}
/**
* Sets the desired speech volume.
*/
/// Sets the desired speech volume.
pub fn set_volume(&mut self, volume: f32) -> Result<&Self, Error> {
let Features {
volume: volume_feature,
..
} = self.supported_features();
if volume_feature {
if volume < self.0.min_volume() || volume > self.0.max_volume() {
let mut backend = self.0.write().unwrap();
if volume < backend.min_volume() || volume > backend.max_volume() {
Err(Error::OutOfRange)
} else {
self.0.set_volume(volume)?;
backend.set_volume(volume)?;
Ok(self)
}
} else {
@ -450,21 +517,50 @@ impl TTS {
}
}
/**
* Returns whether this speech synthesizer is speaking.
*/
/// Returns whether this speech synthesizer is speaking.
pub fn is_speaking(&self) -> Result<bool, Error> {
let Features { is_speaking, .. } = self.supported_features();
if is_speaking {
self.0.is_speaking()
self.0.read().unwrap().is_speaking()
} else {
Err(Error::UnsupportedFeature)
}
}
/**
* Called when this speech synthesizer begins speaking an utterance.
*/
/// Returns list of available voices.
pub fn voices(&self) -> Result<Vec<Voice>, Error> {
let Features { voice, .. } = self.supported_features();
if voice {
self.0.read().unwrap().voices()
} else {
Err(Error::UnsupportedFeature)
}
}
/// Return the current speaking voice.
pub fn voice(&self) -> Result<Option<Voice>, Error> {
let Features { get_voice, .. } = self.supported_features();
if get_voice {
self.0.read().unwrap().voice()
} else {
Err(Error::UnsupportedFeature)
}
}
/// Set speaking voice.
pub fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> {
let Features {
voice: voice_feature,
..
} = self.supported_features();
if voice_feature {
self.0.write().unwrap().set_voice(voice)
} else {
Err(Error::UnsupportedFeature)
}
}
/// Called when this speech synthesizer begins speaking an utterance.
pub fn on_utterance_begin(
&self,
callback: Option<Box<dyn FnMut(UtteranceId)>>,
@ -475,8 +571,8 @@ impl TTS {
} = self.supported_features();
if utterance_callbacks {
let mut callbacks = CALLBACKS.lock().unwrap();
let id = self.0.id().unwrap();
let mut callbacks = callbacks.get_mut(&id).unwrap();
let id = self.0.read().unwrap().id().unwrap();
let callbacks = callbacks.get_mut(&id).unwrap();
callbacks.utterance_begin = callback;
Ok(())
} else {
@ -484,9 +580,7 @@ impl TTS {
}
}
/**
* Called when this speech synthesizer finishes speaking an utterance.
*/
/// Called when this speech synthesizer finishes speaking an utterance.
pub fn on_utterance_end(
&self,
callback: Option<Box<dyn FnMut(UtteranceId)>>,
@ -497,8 +591,8 @@ impl TTS {
} = self.supported_features();
if utterance_callbacks {
let mut callbacks = CALLBACKS.lock().unwrap();
let id = self.0.id().unwrap();
let mut callbacks = callbacks.get_mut(&id).unwrap();
let id = self.0.read().unwrap().id().unwrap();
let callbacks = callbacks.get_mut(&id).unwrap();
callbacks.utterance_end = callback;
Ok(())
} else {
@ -506,9 +600,7 @@ impl TTS {
}
}
/**
* Called when this speech synthesizer is stopped and still has utterances in its queue.
*/
/// Called when this speech synthesizer is stopped and still has utterances in its queue.
pub fn on_utterance_stop(
&self,
callback: Option<Box<dyn FnMut(UtteranceId)>>,
@ -519,21 +611,73 @@ impl TTS {
} = self.supported_features();
if utterance_callbacks {
let mut callbacks = CALLBACKS.lock().unwrap();
let id = self.0.id().unwrap();
let mut callbacks = callbacks.get_mut(&id).unwrap();
let id = self.0.read().unwrap().id().unwrap();
let callbacks = callbacks.get_mut(&id).unwrap();
callbacks.utterance_stop = callback;
Ok(())
} else {
Err(Error::UnsupportedFeature)
}
}
/*
* Returns `true` if a screen reader is available to provide speech.
*/
#[allow(unreachable_code)]
pub fn screen_reader_available() -> bool {
#[cfg(target_os = "windows")]
{
#[cfg(feature = "tolk")]
{
let tolk = Tolk::new();
return tolk.detect_screen_reader().is_some();
}
#[cfg(not(feature = "tolk"))]
return false;
}
false
}
}
impl Drop for TTS {
impl Drop for Tts {
fn drop(&mut self) {
if let Some(id) = self.0.id() {
let mut callbacks = CALLBACKS.lock().unwrap();
callbacks.remove(&id);
if Rc::strong_count(&self.0) <= 1 {
if let Some(id) = self.0.read().unwrap().id() {
let mut callbacks = CALLBACKS.lock().unwrap();
callbacks.remove(&id);
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Gender {
Male,
Female,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Voice {
pub(crate) id: String,
pub(crate) name: String,
pub(crate) gender: Option<Gender>,
pub(crate) language: LanguageTag<String>,
}
impl Voice {
pub fn id(&self) -> String {
self.id.clone()
}
pub fn name(&self) -> String {
self.name.clone()
}
pub fn gender(&self) -> Option<Gender> {
self.gender
}
pub fn language(&self) -> LanguageTag<String> {
self.language.clone()
}
}