├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── Cargo.toml ├── Makefile.toml ├── README.md ├── examples ├── 99bottles.rs ├── android │ ├── .gitignore │ ├── .idea │ │ ├── .gitignore │ │ ├── compiler.xml │ │ ├── gradle.xml │ │ ├── jarRepositories.xml │ │ ├── misc.xml │ │ └── vcs.xml │ ├── app │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src │ │ │ └── main │ │ │ ├── .gitignore │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── rs │ │ │ │ └── tts │ │ │ │ ├── Bridge.java │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ ├── build.gradle │ ├── cargo.toml │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── src │ │ └── lib.rs ├── clone_drop.rs ├── hello_world.rs ├── latency.rs ├── ramble.rs └── web │ ├── .cargo │ └── config │ ├── .gitignore │ ├── Cargo.toml │ ├── index.html │ └── src │ └── main.rs └── src ├── backends ├── android.rs ├── appkit.rs ├── av_foundation.rs ├── mod.rs ├── speech_dispatcher.rs ├── tolk.rs ├── web.rs └── winrt.rs └── lib.rs /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | publish: 10 | name: Publish 11 | runs-on: ubuntu-22.04 12 | env: 13 | CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} 14 | steps: 15 | - uses: actions/checkout@v4 16 | - run: | 17 | sudo apt-get update 18 | sudo apt-get install -y libspeechd-dev 19 | cargo login $CARGO_TOKEN 20 | rustup toolchain install stable 21 | cargo publish 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | check_formatting: 9 | name: Check Formatting 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - uses: actions/checkout@v4 13 | - run: | 14 | rustup toolchain install stable 15 | cargo fmt --all --check 16 | cd examples/web 17 | cargo fmt --all --check 18 | 19 | check: 20 | name: Check 21 | strategy: 22 | matrix: 23 | os: [windows-latest, ubuntu-22.04, macos-latest] 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - uses: actions/checkout@v4 27 | - run: sudo apt-get update; sudo apt-get install -y libspeechd-dev 28 | if: ${{ runner.os == 'Linux' }} 29 | - run: | 30 | rustup toolchain install stable 31 | cargo clippy --all-targets 32 | 33 | check_web: 34 | name: Check Web 35 | runs-on: ubuntu-22.04 36 | steps: 37 | - uses: actions/checkout@v4 38 | - run: | 39 | rustup target add wasm32-unknown-unknown 40 | rustup toolchain install stable 41 | cargo clippy --all-targets --target wasm32-unknown-unknown 42 | 43 | check_android: 44 | name: Check Android 45 | runs-on: ubuntu-22.04 46 | steps: 47 | - uses: actions/checkout@v4 48 | - run: | 49 | rustup target add aarch64-linux-android 50 | rustup toolchain install stable 51 | cargo clippy --all-targets --target aarch64-linux-android 52 | 53 | check_web_example: 54 | name: Check Web Example 55 | runs-on: ubuntu-22.04 56 | steps: 57 | - uses: actions/checkout@v4 58 | - run: | 59 | rustup target add wasm32-unknown-unknown 60 | rustup toolchain install stable 61 | cd examples/web 62 | cargo build --target wasm32-unknown-unknown 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target 3 | *.dll -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tts" 3 | version = "0.26.3" 4 | authors = ["Nolan Darilek "] 5 | repository = "https://github.com/ndarilek/tts-rs" 6 | description = "High-level Text-To-Speech (TTS) interface" 7 | documentation = "https://docs.rs/tts" 8 | license = "MIT" 9 | exclude = ["*.cfg", "*.yml"] 10 | edition = "2021" 11 | 12 | [lib] 13 | crate-type = ["lib", "cdylib", "staticlib"] 14 | 15 | [features] 16 | speech_dispatcher_0_9 = ["speech-dispatcher/0_9"] 17 | speech_dispatcher_0_10 = ["speech-dispatcher/0_10"] 18 | speech_dispatcher_0_11 = ["speech-dispatcher/0_11"] 19 | default = ["speech_dispatcher_0_11"] 20 | 21 | [dependencies] 22 | dyn-clonable = "0.9" 23 | oxilangtag = "0.1" 24 | lazy_static = "1" 25 | log = "0.4" 26 | serde = { version = "1", optional = true, features = ["derive"] } 27 | thiserror = "1" 28 | 29 | [dev-dependencies] 30 | env_logger = "0.11" 31 | 32 | [target.'cfg(windows)'.dependencies] 33 | tolk = { version = "0.5", optional = true } 34 | windows = { version = "0.58", features = [ 35 | "Foundation", 36 | "Foundation_Collections", 37 | "Media_Core", 38 | "Media_Playback", 39 | "Media_SpeechSynthesis", 40 | "Storage_Streams", 41 | ] } 42 | 43 | [target.'cfg(target_os = "linux")'.dependencies] 44 | speech-dispatcher = { version = "0.16", default-features = false } 45 | 46 | [target.'cfg(target_vendor = "apple")'.dependencies] 47 | objc2 = "0.6" 48 | objc2-avf-audio = { version = "0.3", default-features = false, features = [ 49 | "std", 50 | "AVSpeechSynthesis", 51 | ] } 52 | objc2-foundation = { version = "0.3", default-features = false, features = [ 53 | "std", 54 | "NSArray", 55 | "NSDate", 56 | "NSEnumerator", 57 | "NSObjCRuntime", 58 | "NSRunLoop", 59 | "NSString", 60 | ] } 61 | 62 | [target.'cfg(target_os = "macos")'.dependencies] 63 | objc2-app-kit = { version = "0.3", default-features = false, features = [ 64 | "std", 65 | "NSSpeechSynthesizer", 66 | ] } 67 | 68 | [target.wasm32-unknown-unknown.dependencies] 69 | wasm-bindgen = "0.2" 70 | web-sys = { version = "0.3", features = [ 71 | "EventTarget", 72 | "SpeechSynthesis", 73 | "SpeechSynthesisErrorCode", 74 | "SpeechSynthesisErrorEvent", 75 | "SpeechSynthesisEvent", 76 | "SpeechSynthesisUtterance", 77 | "SpeechSynthesisVoice", 78 | "Window", 79 | ] } 80 | 81 | [target.'cfg(target_os="android")'.dependencies] 82 | jni = "0.21" 83 | ndk-context = "0.1" 84 | 85 | [package.metadata.docs.rs] 86 | no-default-features = true 87 | features = ["speech_dispatcher_0_11"] 88 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | [tasks.build-android-example] 2 | script = [ 3 | "cd examples/android", 4 | "./gradlew assembleDebug", 5 | ] 6 | 7 | [tasks.run-android-example] 8 | script = [ 9 | "cd examples/android", 10 | "./gradlew runDebug", 11 | ] 12 | 13 | [tasks.log-android] 14 | command = "adb" 15 | args = ["logcat", "RustStdoutStderr:D", "*:S"] 16 | 17 | [tasks.install-trunk] 18 | install_crate = { crate_name = "trunk", binary = "trunk", test_arg = "--help" } 19 | 20 | [tasks.install-wasm-bindgen-cli] 21 | install_crate = { crate_name = "wasm-bindgen-cli", binary = "wasm-bindgen", test_arg = "--help" } 22 | 23 | [tasks.build-web-example] 24 | dependencies = ["install-trunk", "install-wasm-bindgen-cli"] 25 | cwd = "examples/web" 26 | command = "trunk" 27 | args = ["build"] 28 | 29 | [tasks.run-web-example] 30 | dependencies = ["install-trunk", "install-wasm-bindgen-cli"] 31 | cwd = "examples/web" 32 | command = "trunk" 33 | args = ["serve"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TTS-RS 2 | 3 | This library provides a high-level Text-To-Speech (TTS) interface supporting various backends. Currently supported backends are: 4 | 5 | * Windows 6 | * Screen readers/SAPI via Tolk (requires `tolk` Cargo feature) 7 | * WinRT 8 | * Linux via [Speech Dispatcher](https://freebsoft.org/speechd) 9 | * macOS/iOS/tvOS/watchOS/visionOS. 10 | * AppKit on macOS 10.13 and below. 11 | * AVFoundation on macOS 10.14 and above, and iOS/tvOS/watchOS/visionOS. 12 | * Android 13 | * WebAssembly 14 | 15 | ## Android Setup 16 | 17 | On most platforms, this library is plug-and-play. Because of JNI's complexity, Android setup is a bit more involved. In general, look to the Android example for guidance. Here are some rough steps to get going: 18 | 19 | * Set up _Cargo.toml_ as the example does. Be sure to depend on `ndk-glue`. 20 | * Place _Bridge.java_ appropriately in your app. This is needed to support various Android TTS callbacks. 21 | * Create a main activity similar to _MainActivity.kt_. In particular, you need to derive `android.app.NativeActivity`, and you need a `System.loadLibrary(...)` call appropriate for your app. `System.loadLibrary(...)` is needed to trigger `JNI_OnLoad`. 22 | * * Even though you've loaded the library in your main activity, add a metadata tag to your activity in _AndroidManifest.xml_ referencing it. Yes, this is redundant but necessary. 23 | * Set if your various build.gradle scripts to reference the plugins, dependencies, etc. from the example. In particular, you'll want to set up [cargo-ndk-android-gradle](https://github.com/willir/cargo-ndk-android-gradle/) and either [depend on androidx.annotation](https://developer.android.com/reference/androidx/annotation/package-summary) or otherwise configure your app to keep the class _rs.tts.Bridge_. 24 | 25 | And I think that should about do it. Good luck! 26 | -------------------------------------------------------------------------------- /examples/99bottles.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::{thread, time}; 3 | 4 | use tts::*; 5 | 6 | fn main() -> Result<(), Error> { 7 | env_logger::init(); 8 | let mut tts = Tts::default()?; 9 | let mut bottles = 99; 10 | while bottles > 0 { 11 | tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?; 12 | tts.speak(format!("{} bottles of beer,", bottles), false)?; 13 | tts.speak("Take one down, pass it around", false)?; 14 | tts.speak("Give us a bit to drink this...", false)?; 15 | let time = time::Duration::from_secs(15); 16 | thread::sleep(time); 17 | bottles -= 1; 18 | tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?; 19 | } 20 | let mut _input = String::new(); 21 | // The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context. 22 | // It shouldn't be needed in an app or game that almost certainly has one already. 23 | #[cfg(target_os = "macos")] 24 | { 25 | let run_loop = unsafe { objc2_foundation::NSRunLoop::currentRunLoop() }; 26 | unsafe { run_loop.run() }; 27 | } 28 | io::stdin().read_line(&mut _input)?; 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /examples/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | Cargo.lock 17 | -------------------------------------------------------------------------------- /examples/android/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /examples/android/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/android/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /examples/android/.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /examples/android/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /examples/android/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /examples/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "org.mozilla.rust-android-gradle.rust-android" 4 | } 5 | 6 | android { 7 | namespace "rs.tts" 8 | compileSdkVersion 33 9 | ndkVersion "25.1.8937393" 10 | defaultConfig { 11 | applicationId "rs.tts" 12 | minSdkVersion 21 13 | targetSdkVersion 33 14 | versionCode 1 15 | versionName "1.0" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation "androidx.core:core-ktx:1.2.0" 28 | implementation "androidx.annotation:annotation:1.1.0" 29 | implementation "com.google.android.material:material:1.1.0" 30 | implementation "androidx.constraintlayout:constraintlayout:1.1.3" 31 | } 32 | 33 | apply plugin: "org.mozilla.rust-android-gradle.rust-android" 34 | 35 | cargo { 36 | module = "." 37 | libname = "tts" 38 | targets = ["arm", "x86"] 39 | } 40 | 41 | tasks.whenTaskAdded { task -> 42 | if ((task.name == 'javaPreCompileDebug' || task.name == 'javaPreCompileRelease')) { 43 | task.dependsOn "cargoBuild" 44 | } 45 | } 46 | 47 | project.afterEvaluate { 48 | android.applicationVariants.all { variant -> 49 | task "run${variant.name.capitalize()}"(type: Exec, dependsOn: "install${variant.name.capitalize()}", group: "run") { 50 | commandLine = ["adb", "shell", "monkey", "-p", variant.applicationId + " 1"] 51 | doLast { 52 | println "Launching ${variant.applicationId}" 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /examples/android/app/src/main/.gitignore: -------------------------------------------------------------------------------- 1 | jniLibs 2 | -------------------------------------------------------------------------------- /examples/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/android/app/src/main/java/rs/tts/Bridge.java: -------------------------------------------------------------------------------- 1 | package rs.tts; 2 | 3 | import android.speech.tts.TextToSpeech; 4 | import android.speech.tts.UtteranceProgressListener; 5 | 6 | @androidx.annotation.Keep 7 | public class Bridge extends UtteranceProgressListener implements TextToSpeech.OnInitListener { 8 | public int backendId; 9 | 10 | public Bridge(int backendId) { 11 | this.backendId = backendId; 12 | } 13 | 14 | public native void onInit(int status); 15 | 16 | public native void onStart(String utteranceId); 17 | 18 | public native void onStop(String utteranceId, Boolean interrupted); 19 | 20 | public native void onDone(String utteranceId); 21 | 22 | public native void onError(String utteranceId) ; 23 | 24 | } -------------------------------------------------------------------------------- /examples/android/app/src/main/java/rs/tts/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package rs.tts 2 | 3 | import android.app.NativeActivity 4 | 5 | class MainActivity : NativeActivity() { 6 | companion object { 7 | init { 8 | System.loadLibrary("hello_world") 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /examples/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TTS-RS 3 | -------------------------------------------------------------------------------- /examples/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | maven { 8 | url "https://plugins.gradle.org/m2/" 9 | } 10 | } 11 | } 12 | 13 | plugins { 14 | id "com.android.application" version "7.3.0" apply false 15 | id "com.android.library" version "7.3.0" apply false 16 | id "org.jetbrains.kotlin.android" version "1.7.21" apply false 17 | id "org.mozilla.rust-android-gradle.rust-android" version "0.9.3" apply false 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | mavenCentral() 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } -------------------------------------------------------------------------------- /examples/android/cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello_world" 3 | version = "0.1.0" 4 | authors = ["Nolan Darilek "] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [lib] 10 | crate-type = ["dylib"] 11 | 12 | [dependencies] 13 | ndk-glue = "0.7" 14 | tts = { path = "../.." } -------------------------------------------------------------------------------- /examples/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /examples/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndarilek/tts-rs/8cbcf67eb2ba474398356aea8ea5b5a59510bdf1/examples/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 17:32:22 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 7 | -------------------------------------------------------------------------------- /examples/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /examples/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /examples/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | include ":app" 9 | -------------------------------------------------------------------------------- /examples/android/src/lib.rs: -------------------------------------------------------------------------------- 1 | use tts::*; 2 | 3 | // The `loop {}` below only simulates an app loop. 4 | // Without it, the `TTS` instance gets dropped before callbacks can run. 5 | #[allow(unreachable_code)] 6 | fn run() -> Result<(), Error> { 7 | let mut tts = Tts::default()?; 8 | let Features { 9 | utterance_callbacks, 10 | .. 11 | } = tts.supported_features(); 12 | if utterance_callbacks { 13 | tts.on_utterance_begin(Some(Box::new(|utterance| { 14 | println!("Started speaking {:?}", utterance) 15 | })))?; 16 | tts.on_utterance_end(Some(Box::new(|utterance| { 17 | println!("Finished speaking {:?}", utterance) 18 | })))?; 19 | tts.on_utterance_stop(Some(Box::new(|utterance| { 20 | println!("Stopped speaking {:?}", utterance) 21 | })))?; 22 | } 23 | let Features { is_speaking, .. } = tts.supported_features(); 24 | if is_speaking { 25 | println!("Are we speaking? {}", tts.is_speaking()?); 26 | } 27 | tts.speak("Hello, world.", false)?; 28 | let Features { rate, .. } = tts.supported_features(); 29 | if rate { 30 | let original_rate = tts.get_rate()?; 31 | tts.speak(format!("Current rate: {}", original_rate), false)?; 32 | tts.set_rate(tts.max_rate())?; 33 | tts.speak("This is very fast.", false)?; 34 | tts.set_rate(tts.min_rate())?; 35 | tts.speak("This is very slow.", false)?; 36 | tts.set_rate(tts.normal_rate())?; 37 | tts.speak("This is the normal rate.", false)?; 38 | tts.set_rate(original_rate)?; 39 | } 40 | let Features { pitch, .. } = tts.supported_features(); 41 | if pitch { 42 | let original_pitch = tts.get_pitch()?; 43 | tts.set_pitch(tts.max_pitch())?; 44 | tts.speak("This is high-pitch.", false)?; 45 | tts.set_pitch(tts.min_pitch())?; 46 | tts.speak("This is low pitch.", false)?; 47 | tts.set_pitch(tts.normal_pitch())?; 48 | tts.speak("This is normal pitch.", false)?; 49 | tts.set_pitch(original_pitch)?; 50 | } 51 | let Features { volume, .. } = tts.supported_features(); 52 | if volume { 53 | let original_volume = tts.get_volume()?; 54 | tts.set_volume(tts.max_volume())?; 55 | tts.speak("This is loud!", false)?; 56 | tts.set_volume(tts.min_volume())?; 57 | tts.speak("This is quiet.", false)?; 58 | tts.set_volume(tts.normal_volume())?; 59 | tts.speak("This is normal volume.", false)?; 60 | tts.set_volume(original_volume)?; 61 | } 62 | tts.speak("Goodbye.", false)?; 63 | loop {} 64 | Ok(()) 65 | } 66 | 67 | #[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] 68 | pub fn main() { 69 | run().expect("Failed to run"); 70 | } 71 | -------------------------------------------------------------------------------- /examples/clone_drop.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use tts::*; 4 | 5 | fn main() -> Result<(), Error> { 6 | env_logger::init(); 7 | let tts = Tts::default()?; 8 | if Tts::screen_reader_available() { 9 | println!("A screen reader is available on this platform."); 10 | } else { 11 | println!("No screen reader is available on this platform."); 12 | } 13 | let Features { 14 | utterance_callbacks, 15 | .. 16 | } = tts.supported_features(); 17 | if utterance_callbacks { 18 | tts.on_utterance_begin(Some(Box::new(|utterance| { 19 | println!("Started speaking {:?}", utterance) 20 | })))?; 21 | tts.on_utterance_end(Some(Box::new(|utterance| { 22 | println!("Finished speaking {:?}", utterance) 23 | })))?; 24 | tts.on_utterance_stop(Some(Box::new(|utterance| { 25 | println!("Stopped speaking {:?}", utterance) 26 | })))?; 27 | } 28 | let mut tts_clone = tts.clone(); 29 | drop(tts); 30 | let Features { is_speaking, .. } = tts_clone.supported_features(); 31 | if is_speaking { 32 | println!("Are we speaking? {}", tts_clone.is_speaking()?); 33 | } 34 | tts_clone.speak("Hello, world.", false)?; 35 | let Features { rate, .. } = tts_clone.supported_features(); 36 | if rate { 37 | let original_rate = tts_clone.get_rate()?; 38 | tts_clone.speak(format!("Current rate: {}", original_rate), false)?; 39 | tts_clone.set_rate(tts_clone.max_rate())?; 40 | tts_clone.speak("This is very fast.", false)?; 41 | tts_clone.set_rate(tts_clone.min_rate())?; 42 | tts_clone.speak("This is very slow.", false)?; 43 | tts_clone.set_rate(tts_clone.normal_rate())?; 44 | tts_clone.speak("This is the normal rate.", false)?; 45 | tts_clone.set_rate(original_rate)?; 46 | } 47 | let Features { pitch, .. } = tts_clone.supported_features(); 48 | if pitch { 49 | let original_pitch = tts_clone.get_pitch()?; 50 | tts_clone.set_pitch(tts_clone.max_pitch())?; 51 | tts_clone.speak("This is high-pitch.", false)?; 52 | tts_clone.set_pitch(tts_clone.min_pitch())?; 53 | tts_clone.speak("This is low pitch.", false)?; 54 | tts_clone.set_pitch(tts_clone.normal_pitch())?; 55 | tts_clone.speak("This is normal pitch.", false)?; 56 | tts_clone.set_pitch(original_pitch)?; 57 | } 58 | let Features { volume, .. } = tts_clone.supported_features(); 59 | if volume { 60 | let original_volume = tts_clone.get_volume()?; 61 | tts_clone.set_volume(tts_clone.max_volume())?; 62 | tts_clone.speak("This is loud!", false)?; 63 | tts_clone.set_volume(tts_clone.min_volume())?; 64 | tts_clone.speak("This is quiet.", false)?; 65 | tts_clone.set_volume(tts_clone.normal_volume())?; 66 | tts_clone.speak("This is normal volume.", false)?; 67 | tts_clone.set_volume(original_volume)?; 68 | } 69 | tts_clone.speak("Goodbye.", false)?; 70 | let mut _input = String::new(); 71 | // The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context. 72 | // It shouldn't be needed in an app or game that almost certainly has one already. 73 | #[cfg(target_os = "macos")] 74 | { 75 | let run_loop = unsafe { objc2_foundation::NSRunLoop::currentRunLoop() }; 76 | unsafe { run_loop.run() }; 77 | } 78 | io::stdin().read_line(&mut _input)?; 79 | Ok(()) 80 | } 81 | -------------------------------------------------------------------------------- /examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use tts::*; 4 | 5 | fn main() -> Result<(), Error> { 6 | env_logger::init(); 7 | let mut tts = Tts::default()?; 8 | if Tts::screen_reader_available() { 9 | println!("A screen reader is available on this platform."); 10 | } else { 11 | println!("No screen reader is available on this platform."); 12 | } 13 | let Features { 14 | utterance_callbacks, 15 | .. 16 | } = tts.supported_features(); 17 | if utterance_callbacks { 18 | tts.on_utterance_begin(Some(Box::new(|utterance| { 19 | println!("Started speaking {:?}", utterance) 20 | })))?; 21 | tts.on_utterance_end(Some(Box::new(|utterance| { 22 | println!("Finished speaking {:?}", utterance) 23 | })))?; 24 | tts.on_utterance_stop(Some(Box::new(|utterance| { 25 | println!("Stopped speaking {:?}", utterance) 26 | })))?; 27 | } 28 | let Features { is_speaking, .. } = tts.supported_features(); 29 | if is_speaking { 30 | println!("Are we speaking? {}", tts.is_speaking()?); 31 | } 32 | tts.speak("Hello, world.", false)?; 33 | let Features { rate, .. } = tts.supported_features(); 34 | if rate { 35 | let original_rate = tts.get_rate()?; 36 | tts.speak(format!("Current rate: {}", original_rate), false)?; 37 | tts.set_rate(tts.max_rate())?; 38 | tts.speak("This is very fast.", false)?; 39 | tts.set_rate(tts.min_rate())?; 40 | tts.speak("This is very slow.", false)?; 41 | tts.set_rate(tts.normal_rate())?; 42 | tts.speak("This is the normal rate.", false)?; 43 | tts.set_rate(original_rate)?; 44 | } 45 | let Features { pitch, .. } = tts.supported_features(); 46 | if pitch { 47 | let original_pitch = tts.get_pitch()?; 48 | tts.set_pitch(tts.max_pitch())?; 49 | tts.speak("This is high-pitch.", false)?; 50 | tts.set_pitch(tts.min_pitch())?; 51 | tts.speak("This is low pitch.", false)?; 52 | tts.set_pitch(tts.normal_pitch())?; 53 | tts.speak("This is normal pitch.", false)?; 54 | tts.set_pitch(original_pitch)?; 55 | } 56 | let Features { volume, .. } = tts.supported_features(); 57 | if volume { 58 | let original_volume = tts.get_volume()?; 59 | tts.set_volume(tts.max_volume())?; 60 | tts.speak("This is loud!", false)?; 61 | tts.set_volume(tts.min_volume())?; 62 | tts.speak("This is quiet.", false)?; 63 | tts.set_volume(tts.normal_volume())?; 64 | tts.speak("This is normal volume.", false)?; 65 | tts.set_volume(original_volume)?; 66 | } 67 | let Features { voice, .. } = tts.supported_features(); 68 | if voice { 69 | let voices = tts.voices()?; 70 | println!("Available voices:\n==="); 71 | for v in &voices { 72 | println!("{:?}", v); 73 | } 74 | let Features { get_voice, .. } = tts.supported_features(); 75 | let original_voice = if get_voice { tts.voice()? } else { None }; 76 | for v in &voices { 77 | tts.set_voice(v)?; 78 | tts.speak(format!("This is {}.", v.name()), false)?; 79 | } 80 | if let Some(original_voice) = original_voice { 81 | tts.set_voice(&original_voice)?; 82 | } 83 | } 84 | tts.speak("Goodbye.", false)?; 85 | let mut _input = String::new(); 86 | // The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context. 87 | // It shouldn't be needed in an app or game that almost certainly has one already. 88 | #[cfg(target_os = "macos")] 89 | { 90 | let run_loop = unsafe { objc2_foundation::NSRunLoop::currentRunLoop() }; 91 | unsafe { run_loop.run() }; 92 | } 93 | io::stdin().read_line(&mut _input)?; 94 | Ok(()) 95 | } 96 | -------------------------------------------------------------------------------- /examples/latency.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use tts::*; 4 | 5 | fn main() -> Result<(), Error> { 6 | env_logger::init(); 7 | let mut tts = Tts::default()?; 8 | println!("Press Enter and wait for speech."); 9 | loop { 10 | let mut _input = String::new(); 11 | io::stdin().read_line(&mut _input)?; 12 | tts.speak("Hello, world.", true)?; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/ramble.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time}; 2 | use tts::*; 3 | 4 | fn main() -> Result<(), Error> { 5 | env_logger::init(); 6 | let mut tts = Tts::default()?; 7 | let mut phrase = 1; 8 | loop { 9 | tts.speak(format!("Phrase {}", phrase), false)?; 10 | #[cfg(target_os = "macos")] 11 | { 12 | let run_loop = unsafe { objc2_foundation::NSRunLoop::currentRunLoop() }; 13 | let date = unsafe { objc2_foundation::NSDate::distantFuture() }; 14 | unsafe { run_loop.runMode_beforeDate(objc2_foundation::NSDefaultRunLoopMode, &date) }; 15 | } 16 | let time = time::Duration::from_secs(5); 17 | thread::sleep(time); 18 | phrase += 1; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/web/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" -------------------------------------------------------------------------------- /examples/web/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web" 3 | version = "0.1.0" 4 | authors = ["Nolan Darilek "] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | console_log = "0.2" 11 | log = "0.4" 12 | seed = "0.9" 13 | tts = { path = "../.." } 14 | -------------------------------------------------------------------------------- /examples/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/web/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::wildcard_imports)] 2 | use seed::{prelude::*, *}; 3 | 4 | use tts::Tts; 5 | 6 | #[derive(Clone)] 7 | struct Model { 8 | text: String, 9 | tts: Tts, 10 | } 11 | 12 | #[derive(Clone)] 13 | enum Msg { 14 | TextChanged(String), 15 | RateChanged(String), 16 | PitchChanged(String), 17 | VolumeChanged(String), 18 | VoiceChanged(String), 19 | Speak, 20 | } 21 | 22 | fn init(_: Url, _: &mut impl Orders) -> Model { 23 | let mut tts = Tts::default().unwrap(); 24 | if tts.voices().unwrap().iter().len() > 0 { 25 | if tts.voice().unwrap().is_none() { 26 | tts.set_voice(tts.voices().unwrap().first().unwrap()) 27 | .expect("Failed to set voice"); 28 | } 29 | } 30 | Model { 31 | text: "Hello, world. This is a test of the current text-to-speech values.".into(), 32 | tts, 33 | } 34 | } 35 | 36 | fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { 37 | use Msg::*; 38 | match msg { 39 | TextChanged(text) => model.text = text, 40 | RateChanged(rate) => { 41 | let rate = rate.parse::().unwrap(); 42 | model.tts.set_rate(rate).unwrap(); 43 | } 44 | PitchChanged(pitch) => { 45 | let pitch = pitch.parse::().unwrap(); 46 | model.tts.set_pitch(pitch).unwrap(); 47 | } 48 | VolumeChanged(volume) => { 49 | let volume = volume.parse::().unwrap(); 50 | model.tts.set_volume(volume).unwrap(); 51 | } 52 | VoiceChanged(voice) => { 53 | for v in model.tts.voices().unwrap() { 54 | if v.id() == voice { 55 | model.tts.set_voice(&v).unwrap(); 56 | } 57 | } 58 | } 59 | Speak => { 60 | model.tts.speak(&model.text, false).unwrap(); 61 | } 62 | } 63 | } 64 | 65 | fn view(model: &Model) -> Node { 66 | let should_show_voices = model.tts.voices().unwrap().iter().len() > 0; 67 | form![ 68 | div![label![ 69 | "Text to speak", 70 | input![ 71 | attrs! { 72 | At::Value => model.text, 73 | At::AutoFocus => AtValue::None, 74 | }, 75 | input_ev(Ev::Input, Msg::TextChanged) 76 | ], 77 | ],], 78 | div![label![ 79 | "Rate", 80 | input![ 81 | attrs! { 82 | At::Type => "number", 83 | At::Value => model.tts.get_rate().unwrap(), 84 | At::Min => model.tts.min_rate(), 85 | At::Max => model.tts.max_rate() 86 | }, 87 | input_ev(Ev::Input, Msg::RateChanged) 88 | ], 89 | ],], 90 | div![label![ 91 | "Pitch", 92 | input![ 93 | attrs! { 94 | At::Type => "number", 95 | At::Value => model.tts.get_pitch().unwrap(), 96 | At::Min => model.tts.min_pitch(), 97 | At::Max => model.tts.max_pitch() 98 | }, 99 | input_ev(Ev::Input, Msg::PitchChanged) 100 | ], 101 | ],], 102 | div![label![ 103 | "Volume", 104 | input![ 105 | attrs! { 106 | At::Type => "number", 107 | At::Value => model.tts.get_volume().unwrap(), 108 | At::Min => model.tts.min_volume(), 109 | At::Max => model.tts.max_volume() 110 | }, 111 | input_ev(Ev::Input, Msg::VolumeChanged) 112 | ], 113 | ],], 114 | if should_show_voices { 115 | div![ 116 | label!["Voice"], 117 | select![ 118 | model.tts.voices().unwrap().iter().map(|v| { 119 | let selected = if let Some(voice) = model.tts.voice().unwrap() { 120 | voice.id() == v.id() 121 | } else { 122 | false 123 | }; 124 | option![ 125 | attrs! { 126 | At::Value => v.id() 127 | }, 128 | if selected { 129 | attrs! { 130 | At::Selected => selected 131 | } 132 | } else { 133 | attrs! {} 134 | }, 135 | v.name() 136 | ] 137 | }), 138 | input_ev(Ev::Change, Msg::VoiceChanged) 139 | ] 140 | ] 141 | } else { 142 | div!["Your browser does not seem to support selecting voices."] 143 | }, 144 | button![ 145 | "Speak", 146 | ev(Ev::Click, |e| { 147 | e.prevent_default(); 148 | Msg::Speak 149 | }), 150 | ], 151 | ] 152 | } 153 | 154 | fn main() { 155 | console_log::init().expect("Error initializing logger"); 156 | App::start("app", init, update, view); 157 | } 158 | -------------------------------------------------------------------------------- /src/backends/android.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "android")] 2 | use std::{ 3 | collections::HashSet, 4 | ffi::{CStr, CString}, 5 | os::raw::c_void, 6 | sync::{Mutex, RwLock}, 7 | thread, 8 | time::{Duration, Instant}, 9 | }; 10 | 11 | use jni::{ 12 | objects::{GlobalRef, JObject, JString}, 13 | sys::{jfloat, jint, JNI_VERSION_1_6}, 14 | JNIEnv, JavaVM, 15 | }; 16 | use lazy_static::lazy_static; 17 | use log::{error, info}; 18 | 19 | use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS}; 20 | 21 | lazy_static! { 22 | static ref BRIDGE: Mutex> = Mutex::new(None); 23 | static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); 24 | static ref PENDING_INITIALIZATIONS: RwLock> = RwLock::new(HashSet::new()); 25 | static ref NEXT_UTTERANCE_ID: Mutex = Mutex::new(0); 26 | } 27 | 28 | #[allow(non_snake_case)] 29 | #[no_mangle] 30 | pub extern "system" fn JNI_OnLoad(vm: JavaVM, _: *mut c_void) -> jint { 31 | let mut env = vm.get_env().expect("Cannot get reference to the JNIEnv"); 32 | let b = env 33 | .find_class("rs/tts/Bridge") 34 | .expect("Failed to find `Bridge`"); 35 | let b = env 36 | .new_global_ref(b) 37 | .expect("Failed to create `Bridge` `GlobalRef`"); 38 | let mut bridge = BRIDGE.lock().unwrap(); 39 | *bridge = Some(b); 40 | JNI_VERSION_1_6 41 | } 42 | 43 | #[no_mangle] 44 | #[allow(non_snake_case)] 45 | pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(mut env: JNIEnv, obj: JObject, status: jint) { 46 | let id = env 47 | .get_field(obj, "backendId", "I") 48 | .expect("Failed to get backend ID") 49 | .i() 50 | .expect("Failed to cast to int") as u64; 51 | let mut pending = PENDING_INITIALIZATIONS.write().unwrap(); 52 | (*pending).remove(&id); 53 | if status != 0 { 54 | error!("Failed to initialize TTS engine"); 55 | } 56 | } 57 | 58 | #[no_mangle] 59 | #[allow(non_snake_case)] 60 | pub unsafe extern "C" fn Java_rs_tts_Bridge_onStart( 61 | mut env: JNIEnv, 62 | obj: JObject, 63 | utterance_id: JString, 64 | ) { 65 | let backend_id = env 66 | .get_field(obj, "backendId", "I") 67 | .expect("Failed to get backend ID") 68 | .i() 69 | .expect("Failed to cast to int") as u64; 70 | let backend_id = BackendId::Android(backend_id); 71 | let utterance_id = CString::from(CStr::from_ptr( 72 | env.get_string(&utterance_id).unwrap().as_ptr(), 73 | )) 74 | .into_string() 75 | .unwrap(); 76 | let utterance_id = utterance_id.parse::().unwrap(); 77 | let utterance_id = UtteranceId::Android(utterance_id); 78 | let mut callbacks = CALLBACKS.lock().unwrap(); 79 | let cb = callbacks.get_mut(&backend_id).unwrap(); 80 | if let Some(f) = cb.utterance_begin.as_mut() { 81 | f(utterance_id); 82 | } 83 | } 84 | 85 | #[no_mangle] 86 | #[allow(non_snake_case)] 87 | pub unsafe extern "C" fn Java_rs_tts_Bridge_onStop( 88 | mut env: JNIEnv, 89 | obj: JObject, 90 | utterance_id: JString, 91 | ) { 92 | let backend_id = env 93 | .get_field(obj, "backendId", "I") 94 | .expect("Failed to get backend ID") 95 | .i() 96 | .expect("Failed to cast to int") as u64; 97 | let backend_id = BackendId::Android(backend_id); 98 | let utterance_id = CString::from(CStr::from_ptr( 99 | env.get_string(&utterance_id).unwrap().as_ptr(), 100 | )) 101 | .into_string() 102 | .unwrap(); 103 | let utterance_id = utterance_id.parse::().unwrap(); 104 | let utterance_id = UtteranceId::Android(utterance_id); 105 | let mut callbacks = CALLBACKS.lock().unwrap(); 106 | let cb = callbacks.get_mut(&backend_id).unwrap(); 107 | if let Some(f) = cb.utterance_end.as_mut() { 108 | f(utterance_id); 109 | } 110 | } 111 | 112 | #[no_mangle] 113 | #[allow(non_snake_case)] 114 | pub unsafe extern "C" fn Java_rs_tts_Bridge_onDone( 115 | mut env: JNIEnv, 116 | obj: JObject, 117 | utterance_id: JString, 118 | ) { 119 | let backend_id = env 120 | .get_field(obj, "backendId", "I") 121 | .expect("Failed to get backend ID") 122 | .i() 123 | .expect("Failed to cast to int") as u64; 124 | let backend_id = BackendId::Android(backend_id); 125 | let utterance_id = CString::from(CStr::from_ptr( 126 | env.get_string(&utterance_id).unwrap().as_ptr(), 127 | )) 128 | .into_string() 129 | .unwrap(); 130 | let utterance_id = utterance_id.parse::().unwrap(); 131 | let utterance_id = UtteranceId::Android(utterance_id); 132 | let mut callbacks = CALLBACKS.lock().unwrap(); 133 | let cb = callbacks.get_mut(&backend_id).unwrap(); 134 | if let Some(f) = cb.utterance_stop.as_mut() { 135 | f(utterance_id); 136 | } 137 | } 138 | 139 | #[no_mangle] 140 | #[allow(non_snake_case)] 141 | pub unsafe extern "C" fn Java_rs_tts_Bridge_onError( 142 | mut env: JNIEnv, 143 | obj: JObject, 144 | utterance_id: JString, 145 | ) { 146 | let backend_id = env 147 | .get_field(obj, "backendId", "I") 148 | .expect("Failed to get backend ID") 149 | .i() 150 | .expect("Failed to cast to int") as u64; 151 | let backend_id = BackendId::Android(backend_id); 152 | let utterance_id = CString::from(CStr::from_ptr( 153 | env.get_string(&utterance_id).unwrap().as_ptr(), 154 | )) 155 | .into_string() 156 | .unwrap(); 157 | let utterance_id = utterance_id.parse::().unwrap(); 158 | let utterance_id = UtteranceId::Android(utterance_id); 159 | let mut callbacks = CALLBACKS.lock().unwrap(); 160 | let cb = callbacks.get_mut(&backend_id).unwrap(); 161 | if let Some(f) = cb.utterance_end.as_mut() { 162 | f(utterance_id); 163 | } 164 | } 165 | 166 | #[derive(Clone)] 167 | pub(crate) struct Android { 168 | id: BackendId, 169 | tts: GlobalRef, 170 | rate: f32, 171 | pitch: f32, 172 | } 173 | 174 | impl Android { 175 | pub(crate) fn new() -> Result { 176 | info!("Initializing Android backend"); 177 | let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); 178 | let bid = *backend_id; 179 | let id = BackendId::Android(bid); 180 | *backend_id += 1; 181 | drop(backend_id); 182 | let ctx = ndk_context::android_context(); 183 | let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }?; 184 | let context = unsafe { JObject::from_raw(ctx.context().cast()) }; 185 | let mut env = vm.attach_current_thread_permanently()?; 186 | let bridge = BRIDGE.lock().unwrap(); 187 | if let Some(bridge) = &*bridge { 188 | let bridge = env.new_object(bridge, "(I)V", &[(bid as jint).into()])?; 189 | let tts = env.new_object( 190 | "android/speech/tts/TextToSpeech", 191 | "(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V", 192 | &[(&context).into(), (&bridge).into()], 193 | )?; 194 | env.call_method( 195 | &tts, 196 | "setOnUtteranceProgressListener", 197 | "(Landroid/speech/tts/UtteranceProgressListener;)I", 198 | &[(&bridge).into()], 199 | )?; 200 | { 201 | let mut pending = PENDING_INITIALIZATIONS.write().unwrap(); 202 | (*pending).insert(bid); 203 | } 204 | let tts = env.new_global_ref(tts)?; 205 | // This hack makes my brain bleed. 206 | const MAX_WAIT_TIME: Duration = Duration::from_millis(500); 207 | let start = Instant::now(); 208 | // Wait a max of 500ms for initialization, then return an error to avoid hanging. 209 | loop { 210 | { 211 | let pending = PENDING_INITIALIZATIONS.read().unwrap(); 212 | if !(*pending).contains(&bid) { 213 | break; 214 | } 215 | if start.elapsed() > MAX_WAIT_TIME { 216 | return Err(Error::OperationFailed); 217 | } 218 | } 219 | thread::sleep(Duration::from_millis(5)); 220 | } 221 | Ok(Self { 222 | id, 223 | tts, 224 | rate: 1., 225 | pitch: 1., 226 | }) 227 | } else { 228 | Err(Error::NoneError) 229 | } 230 | } 231 | 232 | fn vm() -> Result { 233 | let ctx = ndk_context::android_context(); 234 | unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) } 235 | } 236 | } 237 | 238 | impl Backend for Android { 239 | fn id(&self) -> Option { 240 | Some(self.id) 241 | } 242 | 243 | fn supported_features(&self) -> Features { 244 | Features { 245 | stop: true, 246 | rate: true, 247 | pitch: true, 248 | volume: false, 249 | is_speaking: true, 250 | utterance_callbacks: true, 251 | voice: false, 252 | get_voice: false, 253 | } 254 | } 255 | 256 | fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { 257 | let vm = Self::vm()?; 258 | let mut env = vm.get_env()?; 259 | let tts = self.tts.as_obj(); 260 | let text = env.new_string(text)?; 261 | let queue_mode = if interrupt { 0 } else { 1 }; 262 | let mut utterance_id = NEXT_UTTERANCE_ID.lock().unwrap(); 263 | let uid = *utterance_id; 264 | *utterance_id += 1; 265 | drop(utterance_id); 266 | let id = UtteranceId::Android(uid); 267 | let uid = env.new_string(uid.to_string())?; 268 | let rv = env.call_method( 269 | tts, 270 | "speak", 271 | "(Ljava/lang/CharSequence;ILandroid/os/Bundle;Ljava/lang/String;)I", 272 | &[ 273 | (&text).into(), 274 | queue_mode.into(), 275 | (&JObject::null()).into(), 276 | (&uid).into(), 277 | ], 278 | )?; 279 | let rv = rv.i()?; 280 | if rv == 0 { 281 | Ok(Some(id)) 282 | } else { 283 | Err(Error::OperationFailed) 284 | } 285 | } 286 | 287 | fn stop(&mut self) -> Result<(), Error> { 288 | let vm = Self::vm()?; 289 | let mut env = vm.get_env()?; 290 | let tts = self.tts.as_obj(); 291 | let rv = env.call_method(tts, "stop", "()I", &[])?; 292 | let rv = rv.i()?; 293 | if rv == 0 { 294 | Ok(()) 295 | } else { 296 | Err(Error::OperationFailed) 297 | } 298 | } 299 | 300 | fn min_rate(&self) -> f32 { 301 | 0.1 302 | } 303 | 304 | fn max_rate(&self) -> f32 { 305 | 10. 306 | } 307 | 308 | fn normal_rate(&self) -> f32 { 309 | 1. 310 | } 311 | 312 | fn get_rate(&self) -> Result { 313 | Ok(self.rate) 314 | } 315 | 316 | fn set_rate(&mut self, rate: f32) -> Result<(), Error> { 317 | let vm = Self::vm()?; 318 | let mut env = vm.get_env()?; 319 | let tts = self.tts.as_obj(); 320 | let rate = rate as jfloat; 321 | let rv = env.call_method(tts, "setSpeechRate", "(F)I", &[rate.into()])?; 322 | let rv = rv.i()?; 323 | if rv == 0 { 324 | self.rate = rate; 325 | Ok(()) 326 | } else { 327 | Err(Error::OperationFailed) 328 | } 329 | } 330 | 331 | fn min_pitch(&self) -> f32 { 332 | 0.1 333 | } 334 | 335 | fn max_pitch(&self) -> f32 { 336 | 2. 337 | } 338 | 339 | fn normal_pitch(&self) -> f32 { 340 | 1. 341 | } 342 | 343 | fn get_pitch(&self) -> Result { 344 | Ok(self.pitch) 345 | } 346 | 347 | fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> { 348 | let vm = Self::vm()?; 349 | let mut env = vm.get_env()?; 350 | let tts = self.tts.as_obj(); 351 | let pitch = pitch as jfloat; 352 | let rv = env.call_method(tts, "setPitch", "(F)I", &[pitch.into()])?; 353 | let rv = rv.i()?; 354 | if rv == 0 { 355 | self.pitch = pitch; 356 | Ok(()) 357 | } else { 358 | Err(Error::OperationFailed) 359 | } 360 | } 361 | 362 | fn min_volume(&self) -> f32 { 363 | todo!() 364 | } 365 | 366 | fn max_volume(&self) -> f32 { 367 | todo!() 368 | } 369 | 370 | fn normal_volume(&self) -> f32 { 371 | todo!() 372 | } 373 | 374 | fn get_volume(&self) -> Result { 375 | todo!() 376 | } 377 | 378 | fn set_volume(&mut self, _volume: f32) -> Result<(), Error> { 379 | todo!() 380 | } 381 | 382 | fn is_speaking(&self) -> Result { 383 | let vm = Self::vm()?; 384 | let mut env = vm.get_env()?; 385 | let tts = self.tts.as_obj(); 386 | let rv = env.call_method(tts, "isSpeaking", "()Z", &[])?; 387 | let rv = rv.z()?; 388 | Ok(rv) 389 | } 390 | 391 | fn voice(&self) -> Result, Error> { 392 | unimplemented!() 393 | } 394 | 395 | fn voices(&self) -> Result, Error> { 396 | unimplemented!() 397 | } 398 | 399 | fn set_voice(&mut self, _voice: &Voice) -> Result<(), Error> { 400 | unimplemented!() 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /src/backends/appkit.rs: -------------------------------------------------------------------------------- 1 | // NSSpeechSynthesizer is deprecated, but we can't use AVSpeechSynthesizer 2 | // on older macOS. 3 | #![allow(deprecated)] 4 | use log::{info, trace}; 5 | use objc2::rc::Retained; 6 | use objc2::{define_class, msg_send, DefinedClass, MainThreadMarker, MainThreadOnly}; 7 | use objc2_app_kit::{NSSpeechSynthesizer, NSSpeechSynthesizerDelegate}; 8 | use objc2_foundation::{NSMutableArray, NSObject, NSObjectProtocol, NSString}; 9 | 10 | use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice}; 11 | 12 | #[derive(Debug)] 13 | struct Ivars { 14 | synth: Retained, 15 | strings: Retained>, 16 | } 17 | 18 | define_class!( 19 | #[derive(Debug)] 20 | #[unsafe(super(NSObject))] 21 | #[name = "MyNSSpeechSynthesizerDelegate"] 22 | #[thread_kind = MainThreadOnly] 23 | #[ivars = Ivars] 24 | struct Delegate; 25 | 26 | unsafe impl NSObjectProtocol for Delegate {} 27 | 28 | unsafe impl NSSpeechSynthesizerDelegate for Delegate { 29 | #[unsafe(method(speechSynthesizer:didFinishSpeaking:))] 30 | fn speech_synthesizer_did_finish_speaking( 31 | &self, 32 | _sender: &NSSpeechSynthesizer, 33 | _finished_speaking: bool, 34 | ) { 35 | let Ivars { strings, synth } = self.ivars(); 36 | if let Some(_str) = strings.firstObject() { 37 | strings.removeObjectAtIndex(0); 38 | if let Some(str) = strings.firstObject() { 39 | unsafe { synth.startSpeakingString(&str) }; 40 | } 41 | } 42 | } 43 | } 44 | ); 45 | 46 | impl Delegate { 47 | fn enqueue_and_speak(&self, string: &NSString) { 48 | let Ivars { strings, synth } = self.ivars(); 49 | strings.addObject(string); 50 | if let Some(str) = strings.firstObject() { 51 | unsafe { synth.startSpeakingString(&str) }; 52 | } 53 | } 54 | 55 | fn clear_queue(&self) { 56 | let strings = &self.ivars().strings; 57 | let mut count = strings.count(); 58 | while count > 0 { 59 | strings.removeObjectAtIndex(0); 60 | count = strings.count(); 61 | } 62 | } 63 | } 64 | 65 | #[derive(Clone, Debug)] 66 | pub(crate) struct AppKit { 67 | synth: Retained, 68 | delegate: Retained, 69 | } 70 | 71 | impl AppKit { 72 | pub(crate) fn new() -> Result { 73 | info!("Initializing AppKit backend"); 74 | let synth = unsafe { NSSpeechSynthesizer::new() }; 75 | 76 | // TODO: It is UB to use NSSpeechSynthesizerDelegate off the main 77 | // thread, we should somehow expose the need to be on the main thread. 78 | // 79 | // Maybe just returning an error? 80 | let mtm = unsafe { MainThreadMarker::new_unchecked() }; 81 | 82 | let delegate = Delegate::alloc(mtm).set_ivars(Ivars { 83 | synth: synth.clone(), 84 | strings: NSMutableArray::new(), 85 | }); 86 | let delegate: Retained = unsafe { msg_send![super(delegate), init] }; 87 | 88 | Ok(AppKit { synth, delegate }) 89 | } 90 | } 91 | 92 | impl Backend for AppKit { 93 | fn id(&self) -> Option { 94 | None 95 | } 96 | 97 | fn supported_features(&self) -> Features { 98 | Features { 99 | stop: true, 100 | rate: true, 101 | volume: true, 102 | is_speaking: true, 103 | ..Default::default() 104 | } 105 | } 106 | 107 | fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { 108 | trace!("speak({}, {})", text, interrupt); 109 | if interrupt { 110 | self.stop()?; 111 | } 112 | let str = NSString::from_str(text); 113 | self.delegate.enqueue_and_speak(&str); 114 | Ok(None) 115 | } 116 | 117 | fn stop(&mut self) -> Result<(), Error> { 118 | trace!("stop()"); 119 | self.delegate.clear_queue(); 120 | unsafe { self.synth.stopSpeaking() }; 121 | Ok(()) 122 | } 123 | 124 | fn min_rate(&self) -> f32 { 125 | 10. 126 | } 127 | 128 | fn max_rate(&self) -> f32 { 129 | 500. 130 | } 131 | 132 | fn normal_rate(&self) -> f32 { 133 | 175. 134 | } 135 | 136 | fn get_rate(&self) -> Result { 137 | let rate: f32 = unsafe { self.synth.rate() }; 138 | Ok(rate) 139 | } 140 | 141 | fn set_rate(&mut self, rate: f32) -> Result<(), Error> { 142 | trace!("set_rate({})", rate); 143 | unsafe { self.synth.setRate(rate) }; 144 | Ok(()) 145 | } 146 | 147 | fn min_pitch(&self) -> f32 { 148 | unimplemented!() 149 | } 150 | 151 | fn max_pitch(&self) -> f32 { 152 | unimplemented!() 153 | } 154 | 155 | fn normal_pitch(&self) -> f32 { 156 | unimplemented!() 157 | } 158 | 159 | fn get_pitch(&self) -> Result { 160 | unimplemented!() 161 | } 162 | 163 | fn set_pitch(&mut self, _pitch: f32) -> Result<(), Error> { 164 | unimplemented!() 165 | } 166 | 167 | fn min_volume(&self) -> f32 { 168 | 0. 169 | } 170 | 171 | fn max_volume(&self) -> f32 { 172 | 1. 173 | } 174 | 175 | fn normal_volume(&self) -> f32 { 176 | 1. 177 | } 178 | 179 | fn get_volume(&self) -> Result { 180 | let volume = unsafe { self.synth.volume() }; 181 | Ok(volume) 182 | } 183 | 184 | fn set_volume(&mut self, volume: f32) -> Result<(), Error> { 185 | unsafe { self.synth.setVolume(volume) }; 186 | Ok(()) 187 | } 188 | 189 | fn is_speaking(&self) -> Result { 190 | let is_speaking = unsafe { self.synth.isSpeaking() }; 191 | Ok(is_speaking) 192 | } 193 | 194 | fn voice(&self) -> Result, Error> { 195 | unimplemented!() 196 | } 197 | 198 | fn voices(&self) -> Result, Error> { 199 | unimplemented!() 200 | } 201 | 202 | fn set_voice(&mut self, _voice: &Voice) -> Result<(), Error> { 203 | unimplemented!() 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/backends/av_foundation.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Mutex; 2 | 3 | use lazy_static::lazy_static; 4 | use log::{info, trace}; 5 | use objc2::rc::Retained; 6 | use objc2::runtime::ProtocolObject; 7 | use objc2::{define_class, msg_send, AllocAnyThread, DefinedClass}; 8 | use objc2_avf_audio::{ 9 | AVSpeechBoundary, AVSpeechSynthesisVoice, AVSpeechSynthesisVoiceGender, AVSpeechSynthesizer, 10 | AVSpeechSynthesizerDelegate, AVSpeechUtterance, 11 | }; 12 | use objc2_foundation::{NSObject, NSObjectProtocol, NSString}; 13 | use oxilangtag::LanguageTag; 14 | 15 | use crate::{Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, CALLBACKS}; 16 | 17 | #[derive(Debug)] 18 | struct Ivars { 19 | backend_id: u64, 20 | } 21 | 22 | define_class!( 23 | #[derive(Debug)] 24 | #[unsafe(super(NSObject))] 25 | #[name = "MyAVSpeechSynthesizerDelegate"] 26 | #[ivars = Ivars] 27 | struct Delegate; 28 | 29 | unsafe impl NSObjectProtocol for Delegate {} 30 | 31 | unsafe impl AVSpeechSynthesizerDelegate for Delegate { 32 | #[unsafe(method(speechSynthesizer:didStartSpeechUtterance:))] 33 | fn speech_synthesizer_did_start_speech_utterance( 34 | &self, 35 | _synthesizer: &AVSpeechSynthesizer, 36 | utterance: &AVSpeechUtterance, 37 | ) { 38 | trace!("speech_synthesizer_did_start_speech_utterance"); 39 | let backend_id = self.ivars().backend_id; 40 | let backend_id = BackendId::AvFoundation(backend_id); 41 | trace!("Locking callbacks"); 42 | let mut callbacks = CALLBACKS.lock().unwrap(); 43 | trace!("Locked"); 44 | let callbacks = callbacks.get_mut(&backend_id).unwrap(); 45 | if let Some(callback) = callbacks.utterance_begin.as_mut() { 46 | trace!("Calling utterance_begin"); 47 | let utterance_id = UtteranceId::AvFoundation(utterance as *const _ as usize); 48 | callback(utterance_id); 49 | trace!("Called"); 50 | } 51 | trace!("Done speech_synthesizer_did_start_speech_utterance"); 52 | } 53 | 54 | #[unsafe(method(speechSynthesizer:didFinishSpeechUtterance:))] 55 | fn speech_synthesizer_did_finish_speech_utterance( 56 | &self, 57 | _synthesizer: &AVSpeechSynthesizer, 58 | utterance: &AVSpeechUtterance, 59 | ) { 60 | trace!("speech_synthesizer_did_finish_speech_utterance"); 61 | let backend_id = self.ivars().backend_id; 62 | let backend_id = BackendId::AvFoundation(backend_id); 63 | trace!("Locking callbacks"); 64 | let mut callbacks = CALLBACKS.lock().unwrap(); 65 | trace!("Locked"); 66 | let callbacks = callbacks.get_mut(&backend_id).unwrap(); 67 | if let Some(callback) = callbacks.utterance_end.as_mut() { 68 | trace!("Calling utterance_end"); 69 | let utterance_id = UtteranceId::AvFoundation(utterance as *const _ as usize); 70 | callback(utterance_id); 71 | trace!("Called"); 72 | } 73 | trace!("Done speech_synthesizer_did_finish_speech_utterance"); 74 | } 75 | 76 | #[unsafe(method(speechSynthesizer:didCancelSpeechUtterance:))] 77 | fn speech_synthesizer_did_cancel_speech_utterance( 78 | &self, 79 | _synthesizer: &AVSpeechSynthesizer, 80 | utterance: &AVSpeechUtterance, 81 | ) { 82 | trace!("speech_synthesizer_did_cancel_speech_utterance"); 83 | let backend_id = self.ivars().backend_id; 84 | let backend_id = BackendId::AvFoundation(backend_id); 85 | trace!("Locking callbacks"); 86 | let mut callbacks = CALLBACKS.lock().unwrap(); 87 | trace!("Locked"); 88 | let callbacks = callbacks.get_mut(&backend_id).unwrap(); 89 | if let Some(callback) = callbacks.utterance_stop.as_mut() { 90 | trace!("Calling utterance_stop"); 91 | let utterance_id = UtteranceId::AvFoundation(utterance as *const _ as usize); 92 | callback(utterance_id); 93 | trace!("Called"); 94 | } 95 | trace!("Done speech_synthesizer_did_cancel_speech_utterance"); 96 | } 97 | } 98 | ); 99 | 100 | #[derive(Clone, Debug)] 101 | pub(crate) struct AvFoundation { 102 | id: BackendId, 103 | /// Kept around to avoid deallocting before we're done. 104 | _delegate: Retained, 105 | synth: Retained, 106 | rate: f32, 107 | volume: f32, 108 | pitch: f32, 109 | voice: Option, 110 | } 111 | 112 | lazy_static! { 113 | static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); 114 | } 115 | 116 | impl AvFoundation { 117 | pub(crate) fn new() -> Result { 118 | info!("Initializing AVFoundation backend"); 119 | 120 | let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); 121 | 122 | trace!("Creating synth"); 123 | let synth = unsafe { AVSpeechSynthesizer::new() }; 124 | trace!("Creating delegate"); 125 | let delegate = Delegate::alloc().set_ivars(Ivars { 126 | backend_id: *backend_id, 127 | }); 128 | let delegate: Retained = unsafe { msg_send![super(delegate), init] }; 129 | trace!("Assigning delegate"); 130 | unsafe { synth.setDelegate(Some(ProtocolObject::from_ref(&*delegate))) }; 131 | 132 | let rv = AvFoundation { 133 | id: BackendId::AvFoundation(*backend_id), 134 | _delegate: delegate, 135 | synth, 136 | rate: 0.5, 137 | volume: 1., 138 | pitch: 1., 139 | voice: None, 140 | }; 141 | *backend_id += 1; 142 | Ok(rv) 143 | } 144 | } 145 | 146 | impl Backend for AvFoundation { 147 | fn id(&self) -> Option { 148 | Some(self.id) 149 | } 150 | 151 | fn supported_features(&self) -> Features { 152 | Features { 153 | stop: true, 154 | rate: true, 155 | pitch: true, 156 | volume: true, 157 | is_speaking: true, 158 | voice: true, 159 | get_voice: false, 160 | utterance_callbacks: true, 161 | } 162 | } 163 | 164 | fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { 165 | trace!("speak({}, {})", text, interrupt); 166 | if interrupt && self.is_speaking()? { 167 | self.stop()?; 168 | } 169 | let utterance; 170 | unsafe { 171 | trace!("Creating utterance string"); 172 | let str = NSString::from_str(text); 173 | trace!("Creating utterance"); 174 | utterance = AVSpeechUtterance::initWithString(AVSpeechUtterance::alloc(), &str); 175 | trace!("Setting rate to {}", self.rate); 176 | utterance.setRate(self.rate); 177 | trace!("Setting volume to {}", self.volume); 178 | utterance.setVolume(self.volume); 179 | trace!("Setting pitch to {}", self.pitch); 180 | utterance.setPitchMultiplier(self.pitch); 181 | if let Some(voice) = &self.voice { 182 | let vid = NSString::from_str(&voice.id()); 183 | let v = AVSpeechSynthesisVoice::voiceWithIdentifier(&*vid) 184 | .ok_or(Error::OperationFailed)?; 185 | utterance.setVoice(Some(&v)); 186 | } 187 | trace!("Enqueuing"); 188 | self.synth.speakUtterance(&utterance); 189 | trace!("Done queuing"); 190 | } 191 | Ok(Some(UtteranceId::AvFoundation( 192 | &*utterance as *const _ as usize, 193 | ))) 194 | } 195 | 196 | fn stop(&mut self) -> Result<(), Error> { 197 | trace!("stop()"); 198 | unsafe { 199 | self.synth 200 | .stopSpeakingAtBoundary(AVSpeechBoundary::Immediate); 201 | } 202 | Ok(()) 203 | } 204 | 205 | fn min_rate(&self) -> f32 { 206 | 0.1 207 | } 208 | 209 | fn max_rate(&self) -> f32 { 210 | 2. 211 | } 212 | 213 | fn normal_rate(&self) -> f32 { 214 | 0.5 215 | } 216 | 217 | fn get_rate(&self) -> Result { 218 | Ok(self.rate) 219 | } 220 | 221 | fn set_rate(&mut self, rate: f32) -> Result<(), Error> { 222 | trace!("set_rate({})", rate); 223 | self.rate = rate; 224 | Ok(()) 225 | } 226 | 227 | fn min_pitch(&self) -> f32 { 228 | 0.5 229 | } 230 | 231 | fn max_pitch(&self) -> f32 { 232 | 2.0 233 | } 234 | 235 | fn normal_pitch(&self) -> f32 { 236 | 1.0 237 | } 238 | 239 | fn get_pitch(&self) -> Result { 240 | Ok(self.pitch) 241 | } 242 | 243 | fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> { 244 | trace!("set_pitch({})", pitch); 245 | self.pitch = pitch; 246 | Ok(()) 247 | } 248 | 249 | fn min_volume(&self) -> f32 { 250 | 0. 251 | } 252 | 253 | fn max_volume(&self) -> f32 { 254 | 1. 255 | } 256 | 257 | fn normal_volume(&self) -> f32 { 258 | 1. 259 | } 260 | 261 | fn get_volume(&self) -> Result { 262 | Ok(self.volume) 263 | } 264 | 265 | fn set_volume(&mut self, volume: f32) -> Result<(), Error> { 266 | trace!("set_volume({})", volume); 267 | self.volume = volume; 268 | Ok(()) 269 | } 270 | 271 | fn is_speaking(&self) -> Result { 272 | trace!("is_speaking()"); 273 | let is_speaking = unsafe { self.synth.isSpeaking() }; 274 | Ok(is_speaking) 275 | } 276 | 277 | fn voice(&self) -> Result, Error> { 278 | unimplemented!() 279 | } 280 | 281 | fn voices(&self) -> Result, Error> { 282 | let voices = unsafe { AVSpeechSynthesisVoice::speechVoices() }; 283 | let rv = voices 284 | .iter() 285 | .map(|v| { 286 | let id = unsafe { v.identifier() }; 287 | let name = unsafe { v.name() }; 288 | let gender = unsafe { v.gender() }; 289 | let gender = match gender { 290 | AVSpeechSynthesisVoiceGender::Male => Some(Gender::Male), 291 | AVSpeechSynthesisVoiceGender::Female => Some(Gender::Female), 292 | _ => None, 293 | }; 294 | let language = unsafe { v.language() }; 295 | let language = language.to_string(); 296 | let language = LanguageTag::parse(language).unwrap(); 297 | Voice { 298 | id: id.to_string(), 299 | name: name.to_string(), 300 | gender, 301 | language, 302 | } 303 | }) 304 | .collect(); 305 | Ok(rv) 306 | } 307 | 308 | fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { 309 | self.voice = Some(voice.clone()); 310 | Ok(()) 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/backends/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "linux")] 2 | mod speech_dispatcher; 3 | 4 | #[cfg(all(windows, feature = "tolk"))] 5 | mod tolk; 6 | 7 | #[cfg(windows)] 8 | mod winrt; 9 | 10 | #[cfg(target_arch = "wasm32")] 11 | mod web; 12 | 13 | #[cfg(target_os = "macos")] 14 | mod appkit; 15 | 16 | #[cfg(target_vendor = "apple")] 17 | mod av_foundation; 18 | 19 | #[cfg(target_os = "android")] 20 | mod android; 21 | 22 | #[cfg(target_os = "linux")] 23 | pub(crate) use self::speech_dispatcher::*; 24 | 25 | #[cfg(all(windows, feature = "tolk"))] 26 | pub(crate) use self::tolk::*; 27 | 28 | #[cfg(windows)] 29 | pub(crate) use self::winrt::*; 30 | 31 | #[cfg(target_arch = "wasm32")] 32 | pub(crate) use self::web::*; 33 | 34 | #[cfg(target_os = "macos")] 35 | pub(crate) use self::appkit::*; 36 | 37 | #[cfg(target_vendor = "apple")] 38 | pub(crate) use self::av_foundation::*; 39 | 40 | #[cfg(target_os = "android")] 41 | pub(crate) use self::android::*; 42 | -------------------------------------------------------------------------------- /src/backends/speech_dispatcher.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "linux")] 2 | use std::{collections::HashMap, sync::Mutex}; 3 | 4 | use lazy_static::*; 5 | use log::{info, trace}; 6 | use oxilangtag::LanguageTag; 7 | use speech_dispatcher::*; 8 | 9 | use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS}; 10 | 11 | #[derive(Clone, Debug)] 12 | pub(crate) struct SpeechDispatcher(Connection); 13 | 14 | lazy_static! { 15 | static ref SPEAKING: Mutex> = { 16 | let m: HashMap = HashMap::new(); 17 | Mutex::new(m) 18 | }; 19 | } 20 | 21 | impl SpeechDispatcher { 22 | pub(crate) fn new() -> std::result::Result { 23 | info!("Initializing SpeechDispatcher backend"); 24 | let connection = speech_dispatcher::Connection::open("tts", "tts", "tts", Mode::Threaded)?; 25 | let sd = SpeechDispatcher(connection); 26 | let mut speaking = SPEAKING.lock().unwrap(); 27 | speaking.insert(sd.0.client_id(), false); 28 | sd.0.on_begin(Some(Box::new(|msg_id, client_id| { 29 | let mut speaking = SPEAKING.lock().unwrap(); 30 | speaking.insert(client_id, true); 31 | let mut callbacks = CALLBACKS.lock().unwrap(); 32 | let backend_id = BackendId::SpeechDispatcher(client_id); 33 | let cb = callbacks.get_mut(&backend_id).unwrap(); 34 | let utterance_id = UtteranceId::SpeechDispatcher(msg_id as u64); 35 | if let Some(f) = cb.utterance_begin.as_mut() { 36 | f(utterance_id); 37 | } 38 | }))); 39 | sd.0.on_end(Some(Box::new(|msg_id, client_id| { 40 | let mut speaking = SPEAKING.lock().unwrap(); 41 | speaking.insert(client_id, false); 42 | let mut callbacks = CALLBACKS.lock().unwrap(); 43 | let backend_id = BackendId::SpeechDispatcher(client_id); 44 | let cb = callbacks.get_mut(&backend_id).unwrap(); 45 | let utterance_id = UtteranceId::SpeechDispatcher(msg_id as u64); 46 | if let Some(f) = cb.utterance_end.as_mut() { 47 | f(utterance_id); 48 | } 49 | }))); 50 | sd.0.on_cancel(Some(Box::new(|msg_id, client_id| { 51 | let mut speaking = SPEAKING.lock().unwrap(); 52 | speaking.insert(client_id, false); 53 | let mut callbacks = CALLBACKS.lock().unwrap(); 54 | let backend_id = BackendId::SpeechDispatcher(client_id); 55 | let cb = callbacks.get_mut(&backend_id).unwrap(); 56 | let utterance_id = UtteranceId::SpeechDispatcher(msg_id as u64); 57 | if let Some(f) = cb.utterance_stop.as_mut() { 58 | f(utterance_id); 59 | } 60 | }))); 61 | sd.0.on_pause(Some(Box::new(|_msg_id, client_id| { 62 | let mut speaking = SPEAKING.lock().unwrap(); 63 | speaking.insert(client_id, false); 64 | }))); 65 | sd.0.on_resume(Some(Box::new(|_msg_id, client_id| { 66 | let mut speaking = SPEAKING.lock().unwrap(); 67 | speaking.insert(client_id, true); 68 | }))); 69 | Ok(sd) 70 | } 71 | } 72 | 73 | impl Backend for SpeechDispatcher { 74 | fn id(&self) -> Option { 75 | Some(BackendId::SpeechDispatcher(self.0.client_id())) 76 | } 77 | 78 | fn supported_features(&self) -> Features { 79 | Features { 80 | stop: true, 81 | rate: true, 82 | pitch: true, 83 | volume: true, 84 | is_speaking: true, 85 | voice: true, 86 | get_voice: false, 87 | utterance_callbacks: true, 88 | } 89 | } 90 | 91 | fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { 92 | trace!("speak({}, {})", text, interrupt); 93 | if interrupt { 94 | self.stop()?; 95 | } 96 | let single_char = text.to_string().capacity() == 1; 97 | if single_char { 98 | self.0.set_punctuation(Punctuation::All)?; 99 | } 100 | let id = self.0.say(Priority::Important, text); 101 | if single_char { 102 | self.0.set_punctuation(Punctuation::None)?; 103 | } 104 | if let Some(id) = id { 105 | Ok(Some(UtteranceId::SpeechDispatcher(id))) 106 | } else { 107 | Err(Error::NoneError) 108 | } 109 | } 110 | 111 | fn stop(&mut self) -> Result<(), Error> { 112 | trace!("stop()"); 113 | self.0.cancel()?; 114 | Ok(()) 115 | } 116 | 117 | fn min_rate(&self) -> f32 { 118 | -100. 119 | } 120 | 121 | fn max_rate(&self) -> f32 { 122 | 100. 123 | } 124 | 125 | fn normal_rate(&self) -> f32 { 126 | 0. 127 | } 128 | 129 | fn get_rate(&self) -> Result { 130 | Ok(self.0.get_voice_rate() as f32) 131 | } 132 | 133 | fn set_rate(&mut self, rate: f32) -> Result<(), Error> { 134 | self.0.set_voice_rate(rate as i32)?; 135 | Ok(()) 136 | } 137 | 138 | fn min_pitch(&self) -> f32 { 139 | -100. 140 | } 141 | 142 | fn max_pitch(&self) -> f32 { 143 | 100. 144 | } 145 | 146 | fn normal_pitch(&self) -> f32 { 147 | 0. 148 | } 149 | 150 | fn get_pitch(&self) -> Result { 151 | Ok(self.0.get_voice_pitch() as f32) 152 | } 153 | 154 | fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> { 155 | self.0.set_voice_pitch(pitch as i32)?; 156 | Ok(()) 157 | } 158 | 159 | fn min_volume(&self) -> f32 { 160 | -100. 161 | } 162 | 163 | fn max_volume(&self) -> f32 { 164 | 100. 165 | } 166 | 167 | fn normal_volume(&self) -> f32 { 168 | 100. 169 | } 170 | 171 | fn get_volume(&self) -> Result { 172 | Ok(self.0.get_volume() as f32) 173 | } 174 | 175 | fn set_volume(&mut self, volume: f32) -> Result<(), Error> { 176 | self.0.set_volume(volume as i32)?; 177 | Ok(()) 178 | } 179 | 180 | fn is_speaking(&self) -> Result { 181 | let speaking = SPEAKING.lock().unwrap(); 182 | let is_speaking = speaking.get(&self.0.client_id()).unwrap(); 183 | Ok(*is_speaking) 184 | } 185 | 186 | fn voices(&self) -> Result, Error> { 187 | let rv = self 188 | .0 189 | .list_synthesis_voices()? 190 | .iter() 191 | .filter(|v| LanguageTag::parse(v.language.clone()).is_ok()) 192 | .map(|v| Voice { 193 | id: v.name.clone(), 194 | name: v.name.clone(), 195 | gender: None, 196 | language: LanguageTag::parse(v.language.clone()).unwrap(), 197 | }) 198 | .collect::>(); 199 | Ok(rv) 200 | } 201 | 202 | fn voice(&self) -> Result, Error> { 203 | unimplemented!() 204 | } 205 | 206 | fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { 207 | for v in self.0.list_synthesis_voices()? { 208 | if v.name == voice.name { 209 | self.0.set_synthesis_voice(&v)?; 210 | return Ok(()); 211 | } 212 | } 213 | Err(Error::OperationFailed) 214 | } 215 | } 216 | 217 | impl Drop for SpeechDispatcher { 218 | fn drop(&mut self) { 219 | let mut speaking = SPEAKING.lock().unwrap(); 220 | speaking.remove(&self.0.client_id()); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/backends/tolk.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(windows, feature = "tolk"))] 2 | use std::sync::Arc; 3 | 4 | use log::{info, trace}; 5 | use tolk::Tolk as TolkPtr; 6 | 7 | use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice}; 8 | 9 | #[derive(Clone, Debug)] 10 | pub(crate) struct Tolk(Arc); 11 | 12 | impl Tolk { 13 | pub(crate) fn new() -> Option { 14 | info!("Initializing Tolk backend"); 15 | let tolk = TolkPtr::new(); 16 | if tolk.detect_screen_reader().is_some() { 17 | Some(Tolk(tolk)) 18 | } else { 19 | None 20 | } 21 | } 22 | } 23 | 24 | impl Backend for Tolk { 25 | fn id(&self) -> Option { 26 | None 27 | } 28 | 29 | fn supported_features(&self) -> Features { 30 | Features { 31 | stop: true, 32 | ..Default::default() 33 | } 34 | } 35 | 36 | fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { 37 | trace!("speak({}, {})", text, interrupt); 38 | self.0.speak(text, interrupt); 39 | Ok(None) 40 | } 41 | 42 | fn stop(&mut self) -> Result<(), Error> { 43 | trace!("stop()"); 44 | self.0.silence(); 45 | Ok(()) 46 | } 47 | 48 | fn min_rate(&self) -> f32 { 49 | unimplemented!() 50 | } 51 | 52 | fn max_rate(&self) -> f32 { 53 | unimplemented!() 54 | } 55 | 56 | fn normal_rate(&self) -> f32 { 57 | unimplemented!() 58 | } 59 | 60 | fn get_rate(&self) -> Result { 61 | unimplemented!(); 62 | } 63 | 64 | fn set_rate(&mut self, _rate: f32) -> Result<(), Error> { 65 | unimplemented!(); 66 | } 67 | 68 | fn min_pitch(&self) -> f32 { 69 | unimplemented!() 70 | } 71 | 72 | fn max_pitch(&self) -> f32 { 73 | unimplemented!() 74 | } 75 | 76 | fn normal_pitch(&self) -> f32 { 77 | unimplemented!() 78 | } 79 | 80 | fn get_pitch(&self) -> Result { 81 | unimplemented!(); 82 | } 83 | 84 | fn set_pitch(&mut self, _pitch: f32) -> Result<(), Error> { 85 | unimplemented!(); 86 | } 87 | 88 | fn min_volume(&self) -> f32 { 89 | unimplemented!() 90 | } 91 | 92 | fn max_volume(&self) -> f32 { 93 | unimplemented!() 94 | } 95 | 96 | fn normal_volume(&self) -> f32 { 97 | unimplemented!() 98 | } 99 | 100 | fn get_volume(&self) -> Result { 101 | unimplemented!(); 102 | } 103 | 104 | fn set_volume(&mut self, _volume: f32) -> Result<(), Error> { 105 | unimplemented!(); 106 | } 107 | 108 | fn is_speaking(&self) -> Result { 109 | unimplemented!() 110 | } 111 | 112 | fn voice(&self) -> Result, Error> { 113 | unimplemented!() 114 | } 115 | 116 | fn voices(&self) -> Result, Error> { 117 | unimplemented!() 118 | } 119 | 120 | fn set_voice(&mut self, _voice: &Voice) -> Result<(), Error> { 121 | unimplemented!() 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/backends/web.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_arch = "wasm32")] 2 | use std::sync::Mutex; 3 | 4 | use lazy_static::lazy_static; 5 | use log::{info, trace}; 6 | use oxilangtag::LanguageTag; 7 | use wasm_bindgen::prelude::*; 8 | use wasm_bindgen::JsCast; 9 | use web_sys::{ 10 | SpeechSynthesisErrorCode, SpeechSynthesisErrorEvent, SpeechSynthesisEvent, 11 | SpeechSynthesisUtterance, SpeechSynthesisVoice, 12 | }; 13 | 14 | use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS}; 15 | 16 | #[derive(Clone, Debug)] 17 | pub struct Web { 18 | id: BackendId, 19 | rate: f32, 20 | pitch: f32, 21 | volume: f32, 22 | voice: Option, 23 | } 24 | 25 | lazy_static! { 26 | static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); 27 | static ref UTTERANCE_MAPPINGS: Mutex> = Mutex::new(Vec::new()); 28 | static ref NEXT_UTTERANCE_ID: Mutex = Mutex::new(0); 29 | } 30 | 31 | impl Web { 32 | pub fn new() -> Result { 33 | info!("Initializing Web backend"); 34 | let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); 35 | let rv = Web { 36 | id: BackendId::Web(*backend_id), 37 | rate: 1., 38 | pitch: 1., 39 | volume: 1., 40 | voice: None, 41 | }; 42 | *backend_id += 1; 43 | Ok(rv) 44 | } 45 | } 46 | 47 | impl Backend for Web { 48 | fn id(&self) -> Option { 49 | Some(self.id) 50 | } 51 | 52 | fn supported_features(&self) -> Features { 53 | Features { 54 | stop: true, 55 | rate: true, 56 | pitch: true, 57 | volume: true, 58 | is_speaking: true, 59 | voice: true, 60 | get_voice: true, 61 | utterance_callbacks: true, 62 | } 63 | } 64 | 65 | fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { 66 | trace!("speak({}, {})", text, interrupt); 67 | let utterance = SpeechSynthesisUtterance::new_with_text(text).unwrap(); 68 | utterance.set_rate(self.rate); 69 | utterance.set_pitch(self.pitch); 70 | utterance.set_volume(self.volume); 71 | if self.voice.is_some() { 72 | utterance.set_voice(self.voice.as_ref()); 73 | } 74 | let id = self.id().unwrap(); 75 | let mut uid = NEXT_UTTERANCE_ID.lock().unwrap(); 76 | let utterance_id = UtteranceId::Web(*uid); 77 | *uid += 1; 78 | drop(uid); 79 | let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); 80 | mappings.push((self.id, utterance_id)); 81 | drop(mappings); 82 | let callback = Closure::wrap(Box::new(move |_evt: SpeechSynthesisEvent| { 83 | let mut callbacks = CALLBACKS.lock().unwrap(); 84 | let callback = callbacks.get_mut(&id).unwrap(); 85 | if let Some(f) = callback.utterance_begin.as_mut() { 86 | f(utterance_id); 87 | } 88 | }) as Box); 89 | utterance.set_onstart(Some(callback.as_ref().unchecked_ref())); 90 | let callback = Closure::wrap(Box::new(move |_evt: SpeechSynthesisEvent| { 91 | let mut callbacks = CALLBACKS.lock().unwrap(); 92 | let callback = callbacks.get_mut(&id).unwrap(); 93 | if let Some(f) = callback.utterance_end.as_mut() { 94 | f(utterance_id); 95 | } 96 | let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); 97 | mappings.retain(|v| v.1 != utterance_id); 98 | }) as Box); 99 | utterance.set_onend(Some(callback.as_ref().unchecked_ref())); 100 | let callback = Closure::wrap(Box::new(move |evt: SpeechSynthesisErrorEvent| { 101 | if evt.error() == SpeechSynthesisErrorCode::Canceled { 102 | let mut callbacks = CALLBACKS.lock().unwrap(); 103 | let callback = callbacks.get_mut(&id).unwrap(); 104 | if let Some(f) = callback.utterance_stop.as_mut() { 105 | f(utterance_id); 106 | } 107 | } 108 | let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); 109 | mappings.retain(|v| v.1 != utterance_id); 110 | }) as Box); 111 | utterance.set_onerror(Some(callback.as_ref().unchecked_ref())); 112 | if interrupt { 113 | self.stop()?; 114 | } 115 | if let Some(window) = web_sys::window() { 116 | let speech_synthesis = window.speech_synthesis().unwrap(); 117 | speech_synthesis.speak(&utterance); 118 | Ok(Some(utterance_id)) 119 | } else { 120 | Err(Error::NoneError) 121 | } 122 | } 123 | 124 | fn stop(&mut self) -> Result<(), Error> { 125 | trace!("stop()"); 126 | if let Some(window) = web_sys::window() { 127 | let speech_synthesis = window.speech_synthesis().unwrap(); 128 | speech_synthesis.cancel(); 129 | } 130 | Ok(()) 131 | } 132 | 133 | fn min_rate(&self) -> f32 { 134 | 0.1 135 | } 136 | 137 | fn max_rate(&self) -> f32 { 138 | 10. 139 | } 140 | 141 | fn normal_rate(&self) -> f32 { 142 | 1. 143 | } 144 | 145 | fn get_rate(&self) -> Result { 146 | Ok(self.rate) 147 | } 148 | 149 | fn set_rate(&mut self, rate: f32) -> Result<(), Error> { 150 | self.rate = rate; 151 | Ok(()) 152 | } 153 | 154 | fn min_pitch(&self) -> f32 { 155 | 0. 156 | } 157 | 158 | fn max_pitch(&self) -> f32 { 159 | 2. 160 | } 161 | 162 | fn normal_pitch(&self) -> f32 { 163 | 1. 164 | } 165 | 166 | fn get_pitch(&self) -> Result { 167 | Ok(self.pitch) 168 | } 169 | 170 | fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> { 171 | self.pitch = pitch; 172 | Ok(()) 173 | } 174 | 175 | fn min_volume(&self) -> f32 { 176 | 0. 177 | } 178 | 179 | fn max_volume(&self) -> f32 { 180 | 1. 181 | } 182 | 183 | fn normal_volume(&self) -> f32 { 184 | 1. 185 | } 186 | 187 | fn get_volume(&self) -> Result { 188 | Ok(self.volume) 189 | } 190 | 191 | fn set_volume(&mut self, volume: f32) -> Result<(), Error> { 192 | self.volume = volume; 193 | Ok(()) 194 | } 195 | 196 | fn is_speaking(&self) -> Result { 197 | trace!("is_speaking()"); 198 | if let Some(window) = web_sys::window() { 199 | match window.speech_synthesis() { 200 | Ok(speech_synthesis) => Ok(speech_synthesis.speaking()), 201 | Err(e) => Err(Error::JavaScriptError(e)), 202 | } 203 | } else { 204 | Err(Error::NoneError) 205 | } 206 | } 207 | 208 | fn voice(&self) -> Result, Error> { 209 | if let Some(voice) = &self.voice { 210 | Ok(Some(voice.clone().into())) 211 | } else { 212 | if let Some(window) = web_sys::window() { 213 | let speech_synthesis = window.speech_synthesis().unwrap(); 214 | for voice in speech_synthesis.get_voices().iter() { 215 | let voice: SpeechSynthesisVoice = voice.into(); 216 | if voice.default() { 217 | return Ok(Some(voice.into())); 218 | } 219 | } 220 | } else { 221 | return Err(Error::NoneError); 222 | } 223 | Ok(None) 224 | } 225 | } 226 | 227 | fn voices(&self) -> Result, Error> { 228 | if let Some(window) = web_sys::window() { 229 | let speech_synthesis = window.speech_synthesis().unwrap(); 230 | let mut rv: Vec = vec![]; 231 | for v in speech_synthesis.get_voices().iter() { 232 | let v: SpeechSynthesisVoice = v.into(); 233 | rv.push(v.into()); 234 | } 235 | Ok(rv) 236 | } else { 237 | Err(Error::NoneError) 238 | } 239 | } 240 | 241 | fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { 242 | if let Some(window) = web_sys::window() { 243 | let speech_synthesis = window.speech_synthesis().unwrap(); 244 | for v in speech_synthesis.get_voices().iter() { 245 | let v: SpeechSynthesisVoice = v.into(); 246 | if v.voice_uri() == voice.id { 247 | self.voice = Some(v); 248 | return Ok(()); 249 | } 250 | } 251 | Err(Error::OperationFailed) 252 | } else { 253 | Err(Error::NoneError) 254 | } 255 | } 256 | } 257 | 258 | impl Drop for Web { 259 | fn drop(&mut self) { 260 | let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); 261 | mappings.retain(|v| v.0 != self.id); 262 | } 263 | } 264 | 265 | impl From for Voice { 266 | fn from(other: SpeechSynthesisVoice) -> Self { 267 | let language = LanguageTag::parse(other.lang()).unwrap(); 268 | Voice { 269 | id: other.voice_uri(), 270 | name: other.name(), 271 | gender: None, 272 | language, 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/backends/winrt.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | use std::{ 3 | collections::{HashMap, VecDeque}, 4 | sync::Mutex, 5 | }; 6 | 7 | use lazy_static::lazy_static; 8 | use log::{info, trace}; 9 | use oxilangtag::LanguageTag; 10 | use windows::{ 11 | Foundation::TypedEventHandler, 12 | Media::{ 13 | Core::MediaSource, 14 | Playback::{MediaPlayer, MediaPlayerAudioCategory}, 15 | SpeechSynthesis::{SpeechSynthesizer, VoiceGender, VoiceInformation}, 16 | }, 17 | }; 18 | 19 | use crate::{Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, CALLBACKS}; 20 | 21 | impl From for Error { 22 | fn from(e: windows::core::Error) -> Self { 23 | Error::WinRt(e) 24 | } 25 | } 26 | 27 | #[derive(Clone)] 28 | pub struct WinRt { 29 | id: BackendId, 30 | synth: SpeechSynthesizer, 31 | player: MediaPlayer, 32 | rate: f32, 33 | pitch: f32, 34 | volume: f32, 35 | voice: VoiceInformation, 36 | } 37 | 38 | struct Utterance { 39 | id: UtteranceId, 40 | text: String, 41 | rate: f32, 42 | pitch: f32, 43 | volume: f32, 44 | voice: VoiceInformation, 45 | } 46 | 47 | lazy_static! { 48 | static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); 49 | static ref NEXT_UTTERANCE_ID: Mutex = Mutex::new(0); 50 | static ref BACKEND_TO_SPEECH_SYNTHESIZER: Mutex> = { 51 | let v: HashMap = HashMap::new(); 52 | Mutex::new(v) 53 | }; 54 | static ref BACKEND_TO_MEDIA_PLAYER: Mutex> = { 55 | let v: HashMap = HashMap::new(); 56 | Mutex::new(v) 57 | }; 58 | static ref UTTERANCES: Mutex>> = { 59 | let utterances: HashMap> = HashMap::new(); 60 | Mutex::new(utterances) 61 | }; 62 | } 63 | 64 | impl WinRt { 65 | pub fn new() -> std::result::Result { 66 | info!("Initializing WinRT backend"); 67 | let synth = SpeechSynthesizer::new()?; 68 | let player = MediaPlayer::new()?; 69 | player.SetRealTimePlayback(true)?; 70 | player.SetAudioCategory(MediaPlayerAudioCategory::Speech)?; 71 | let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); 72 | let bid = BackendId::WinRt(*backend_id); 73 | *backend_id += 1; 74 | drop(backend_id); 75 | { 76 | let mut utterances = UTTERANCES.lock().unwrap(); 77 | utterances.insert(bid, VecDeque::new()); 78 | } 79 | let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); 80 | backend_to_media_player.insert(bid, player.clone()); 81 | drop(backend_to_media_player); 82 | let mut backend_to_speech_synthesizer = BACKEND_TO_SPEECH_SYNTHESIZER.lock().unwrap(); 83 | backend_to_speech_synthesizer.insert(bid, synth.clone()); 84 | drop(backend_to_speech_synthesizer); 85 | let bid_clone = bid; 86 | player.MediaEnded(&TypedEventHandler::new( 87 | move |sender: &Option, _args| { 88 | if let Some(sender) = sender { 89 | let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); 90 | let id = backend_to_media_player.iter().find(|v| v.1 == sender); 91 | if let Some((id, _)) = id { 92 | let mut utterances = UTTERANCES.lock().unwrap(); 93 | if let Some(utterances) = utterances.get_mut(id) { 94 | if let Some(utterance) = utterances.pop_front() { 95 | let mut callbacks = CALLBACKS.lock().unwrap(); 96 | let callbacks = callbacks.get_mut(id).unwrap(); 97 | if let Some(callback) = callbacks.utterance_end.as_mut() { 98 | callback(utterance.id); 99 | } 100 | if let Some(utterance) = utterances.front() { 101 | let backend_to_speech_synthesizer = 102 | BACKEND_TO_SPEECH_SYNTHESIZER.lock().unwrap(); 103 | let id = backend_to_speech_synthesizer 104 | .iter() 105 | .find(|v| *v.0 == bid_clone); 106 | if let Some((_, tts)) = id { 107 | tts.Options()?.SetSpeakingRate(utterance.rate.into())?; 108 | tts.Options()?.SetAudioPitch(utterance.pitch.into())?; 109 | tts.Options()?.SetAudioVolume(utterance.volume.into())?; 110 | tts.SetVoice(&utterance.voice)?; 111 | let text = &utterance.text; 112 | let stream = 113 | tts.SynthesizeTextToStreamAsync(&text.into())?.get()?; 114 | let content_type = stream.ContentType()?; 115 | let source = 116 | MediaSource::CreateFromStream(&stream, &content_type)?; 117 | sender.SetSource(&source)?; 118 | sender.Play()?; 119 | if let Some(callback) = callbacks.utterance_begin.as_mut() { 120 | callback(utterance.id); 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | Ok(()) 129 | }, 130 | ))?; 131 | Ok(Self { 132 | id: bid, 133 | synth, 134 | player, 135 | rate: 1., 136 | pitch: 1., 137 | volume: 1., 138 | voice: SpeechSynthesizer::DefaultVoice()?, 139 | }) 140 | } 141 | } 142 | 143 | impl Backend for WinRt { 144 | fn id(&self) -> Option { 145 | Some(self.id) 146 | } 147 | 148 | fn supported_features(&self) -> Features { 149 | Features { 150 | stop: true, 151 | rate: true, 152 | pitch: true, 153 | volume: true, 154 | is_speaking: true, 155 | voice: true, 156 | get_voice: true, 157 | utterance_callbacks: true, 158 | } 159 | } 160 | 161 | fn speak( 162 | &mut self, 163 | text: &str, 164 | interrupt: bool, 165 | ) -> std::result::Result, Error> { 166 | if interrupt && self.is_speaking()? { 167 | self.stop()?; 168 | } 169 | let utterance_id = { 170 | let mut uid = NEXT_UTTERANCE_ID.lock().unwrap(); 171 | let utterance_id = UtteranceId::WinRt(*uid); 172 | *uid += 1; 173 | utterance_id 174 | }; 175 | let mut no_utterances = false; 176 | { 177 | let mut utterances = UTTERANCES.lock().unwrap(); 178 | if let Some(utterances) = utterances.get_mut(&self.id) { 179 | no_utterances = utterances.is_empty(); 180 | let utterance = Utterance { 181 | id: utterance_id, 182 | text: text.into(), 183 | rate: self.rate, 184 | pitch: self.pitch, 185 | volume: self.volume, 186 | voice: self.voice.clone(), 187 | }; 188 | utterances.push_back(utterance); 189 | } 190 | } 191 | if no_utterances { 192 | self.synth.Options()?.SetSpeakingRate(self.rate.into())?; 193 | self.synth.Options()?.SetAudioPitch(self.pitch.into())?; 194 | self.synth.Options()?.SetAudioVolume(self.volume.into())?; 195 | self.synth.SetVoice(&self.voice)?; 196 | let stream = self 197 | .synth 198 | .SynthesizeTextToStreamAsync(&text.into())? 199 | .get()?; 200 | let content_type = stream.ContentType()?; 201 | let source = MediaSource::CreateFromStream(&stream, &content_type)?; 202 | self.player.SetSource(&source)?; 203 | self.player.Play()?; 204 | let mut callbacks = CALLBACKS.lock().unwrap(); 205 | let callbacks = callbacks.get_mut(&self.id).unwrap(); 206 | if let Some(callback) = callbacks.utterance_begin.as_mut() { 207 | callback(utterance_id); 208 | } 209 | } 210 | Ok(Some(utterance_id)) 211 | } 212 | 213 | fn stop(&mut self) -> std::result::Result<(), Error> { 214 | trace!("stop()"); 215 | if !self.is_speaking()? { 216 | return Ok(()); 217 | } 218 | let mut utterances = UTTERANCES.lock().unwrap(); 219 | if let Some(utterances) = utterances.get(&self.id) { 220 | let mut callbacks = CALLBACKS.lock().unwrap(); 221 | let callbacks = callbacks.get_mut(&self.id).unwrap(); 222 | if let Some(callback) = callbacks.utterance_stop.as_mut() { 223 | for utterance in utterances { 224 | callback(utterance.id); 225 | } 226 | } 227 | } 228 | if let Some(utterances) = utterances.get_mut(&self.id) { 229 | utterances.clear(); 230 | } 231 | self.player.Pause()?; 232 | Ok(()) 233 | } 234 | 235 | fn min_rate(&self) -> f32 { 236 | 0.5 237 | } 238 | 239 | fn max_rate(&self) -> f32 { 240 | 6.0 241 | } 242 | 243 | fn normal_rate(&self) -> f32 { 244 | 1. 245 | } 246 | 247 | fn get_rate(&self) -> std::result::Result { 248 | let rate = self.synth.Options()?.SpeakingRate()?; 249 | Ok(rate as f32) 250 | } 251 | 252 | fn set_rate(&mut self, rate: f32) -> std::result::Result<(), Error> { 253 | self.rate = rate; 254 | Ok(()) 255 | } 256 | 257 | fn min_pitch(&self) -> f32 { 258 | 0. 259 | } 260 | 261 | fn max_pitch(&self) -> f32 { 262 | 2. 263 | } 264 | 265 | fn normal_pitch(&self) -> f32 { 266 | 1. 267 | } 268 | 269 | fn get_pitch(&self) -> std::result::Result { 270 | let pitch = self.synth.Options()?.AudioPitch()?; 271 | Ok(pitch as f32) 272 | } 273 | 274 | fn set_pitch(&mut self, pitch: f32) -> std::result::Result<(), Error> { 275 | self.pitch = pitch; 276 | Ok(()) 277 | } 278 | 279 | fn min_volume(&self) -> f32 { 280 | 0. 281 | } 282 | 283 | fn max_volume(&self) -> f32 { 284 | 1. 285 | } 286 | 287 | fn normal_volume(&self) -> f32 { 288 | 1. 289 | } 290 | 291 | fn get_volume(&self) -> std::result::Result { 292 | let volume = self.synth.Options()?.AudioVolume()?; 293 | Ok(volume as f32) 294 | } 295 | 296 | fn set_volume(&mut self, volume: f32) -> std::result::Result<(), Error> { 297 | self.volume = volume; 298 | Ok(()) 299 | } 300 | 301 | fn is_speaking(&self) -> std::result::Result { 302 | let utterances = UTTERANCES.lock().unwrap(); 303 | let utterances = utterances.get(&self.id).unwrap(); 304 | Ok(!utterances.is_empty()) 305 | } 306 | 307 | fn voice(&self) -> Result, Error> { 308 | let voice = self.synth.Voice()?; 309 | let voice = voice.try_into()?; 310 | Ok(Some(voice)) 311 | } 312 | 313 | fn voices(&self) -> Result, Error> { 314 | let mut rv: Vec = vec![]; 315 | for voice in SpeechSynthesizer::AllVoices()? { 316 | rv.push(voice.try_into()?); 317 | } 318 | Ok(rv) 319 | } 320 | 321 | fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { 322 | for v in SpeechSynthesizer::AllVoices()? { 323 | let vid: String = v.Id()?.try_into()?; 324 | if vid == voice.id { 325 | self.voice = v; 326 | return Ok(()); 327 | } 328 | } 329 | Err(Error::OperationFailed) 330 | } 331 | } 332 | 333 | impl Drop for WinRt { 334 | fn drop(&mut self) { 335 | let id = self.id; 336 | let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); 337 | backend_to_media_player.remove(&id); 338 | let mut backend_to_speech_synthesizer = BACKEND_TO_SPEECH_SYNTHESIZER.lock().unwrap(); 339 | backend_to_speech_synthesizer.remove(&id); 340 | let mut utterances = UTTERANCES.lock().unwrap(); 341 | utterances.remove(&id); 342 | } 343 | } 344 | 345 | impl TryInto for VoiceInformation { 346 | type Error = Error; 347 | 348 | fn try_into(self) -> Result { 349 | let gender = self.Gender()?; 350 | let gender = if gender == VoiceGender::Male { 351 | Gender::Male 352 | } else { 353 | Gender::Female 354 | }; 355 | let language: String = self.Language()?.try_into()?; 356 | let language = LanguageTag::parse(language).unwrap(); 357 | Ok(Voice { 358 | id: self.Id()?.try_into()?, 359 | name: self.DisplayName()?.try_into()?, 360 | gender: Some(gender), 361 | language, 362 | }) 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! * a Text-To-Speech (TTS) library providing high-level interfaces to a variety of backends. 2 | //! * Currently supported backends are: 3 | //! * * Windows 4 | //! * * Screen readers/SAPI via Tolk (requires `tolk` Cargo feature) 5 | //! * * WinRT 6 | //! * * Linux via [Speech Dispatcher](https://freebsoft.org/speechd) 7 | //! * * macOS/iOS/tvOS/watchOS/visionOS 8 | //! * * AppKit on macOS 10.13 and below. 9 | //! * * AVFoundation on macOS 10.14 and above, and iOS/tvOS/watchOS/visionOS. 10 | //! * * Android 11 | //! * * WebAssembly 12 | 13 | use std::collections::HashMap; 14 | use std::fmt; 15 | use std::rc::Rc; 16 | #[cfg(windows)] 17 | use std::string::FromUtf16Error; 18 | use std::sync::Mutex; 19 | use std::{boxed::Box, sync::RwLock}; 20 | 21 | use dyn_clonable::*; 22 | use lazy_static::lazy_static; 23 | pub use oxilangtag::LanguageTag; 24 | #[cfg(target_os = "linux")] 25 | use speech_dispatcher::Error as SpeechDispatcherError; 26 | use thiserror::Error; 27 | #[cfg(all(windows, feature = "tolk"))] 28 | use tolk::Tolk; 29 | 30 | mod backends; 31 | 32 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 33 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 34 | pub enum Backends { 35 | #[cfg(target_os = "android")] 36 | Android, 37 | #[cfg(target_os = "macos")] 38 | AppKit, 39 | #[cfg(target_vendor = "apple")] 40 | AvFoundation, 41 | #[cfg(target_os = "linux")] 42 | SpeechDispatcher, 43 | #[cfg(all(windows, feature = "tolk"))] 44 | Tolk, 45 | #[cfg(target_arch = "wasm32")] 46 | Web, 47 | #[cfg(windows)] 48 | WinRt, 49 | } 50 | 51 | impl fmt::Display for Backends { 52 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 53 | match self { 54 | #[cfg(target_os = "android")] 55 | Backends::Android => writeln!(f, "Android"), 56 | #[cfg(target_os = "macos")] 57 | Backends::AppKit => writeln!(f, "AppKit"), 58 | #[cfg(target_vendor = "apple")] 59 | Backends::AvFoundation => writeln!(f, "AVFoundation"), 60 | #[cfg(target_os = "linux")] 61 | Backends::SpeechDispatcher => writeln!(f, "Speech Dispatcher"), 62 | #[cfg(all(windows, feature = "tolk"))] 63 | Backends::Tolk => writeln!(f, "Tolk"), 64 | #[cfg(target_arch = "wasm32")] 65 | Backends::Web => writeln!(f, "Web"), 66 | #[cfg(windows)] 67 | Backends::WinRt => writeln!(f, "Windows Runtime"), 68 | } 69 | } 70 | } 71 | 72 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 73 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 74 | pub enum BackendId { 75 | #[cfg(target_os = "android")] 76 | Android(u64), 77 | #[cfg(target_vendor = "apple")] 78 | AvFoundation(u64), 79 | #[cfg(target_os = "linux")] 80 | SpeechDispatcher(usize), 81 | #[cfg(target_arch = "wasm32")] 82 | Web(u64), 83 | #[cfg(windows)] 84 | WinRt(u64), 85 | } 86 | 87 | impl fmt::Display for BackendId { 88 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 89 | match self { 90 | #[cfg(target_os = "android")] 91 | BackendId::Android(id) => writeln!(f, "Android({id})"), 92 | #[cfg(target_vendor = "apple")] 93 | BackendId::AvFoundation(id) => writeln!(f, "AvFoundation({id})"), 94 | #[cfg(target_os = "linux")] 95 | BackendId::SpeechDispatcher(id) => writeln!(f, "SpeechDispatcher({id})"), 96 | #[cfg(target_arch = "wasm32")] 97 | BackendId::Web(id) => writeln!(f, "Web({id})"), 98 | #[cfg(windows)] 99 | BackendId::WinRt(id) => writeln!(f, "WinRT({id})"), 100 | } 101 | } 102 | } 103 | 104 | #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)] 105 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 106 | pub enum UtteranceId { 107 | #[cfg(target_os = "android")] 108 | Android(u64), 109 | #[cfg(target_vendor = "apple")] 110 | AvFoundation(usize), 111 | #[cfg(target_os = "linux")] 112 | SpeechDispatcher(u64), 113 | #[cfg(target_arch = "wasm32")] 114 | Web(u64), 115 | #[cfg(windows)] 116 | WinRt(u64), 117 | } 118 | 119 | impl fmt::Display for UtteranceId { 120 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 121 | match self { 122 | #[cfg(target_os = "android")] 123 | UtteranceId::Android(id) => writeln!(f, "Android({id})"), 124 | #[cfg(target_os = "linux")] 125 | UtteranceId::SpeechDispatcher(id) => writeln!(f, "SpeechDispatcher({id})"), 126 | #[cfg(target_vendor = "apple")] 127 | UtteranceId::AvFoundation(id) => writeln!(f, "AvFoundation({id})"), 128 | #[cfg(target_arch = "wasm32")] 129 | UtteranceId::Web(id) => writeln!(f, "Web({})", id), 130 | #[cfg(windows)] 131 | UtteranceId::WinRt(id) => writeln!(f, "WinRt({id})"), 132 | } 133 | } 134 | } 135 | 136 | #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] 137 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 138 | pub struct Features { 139 | pub is_speaking: bool, 140 | pub pitch: bool, 141 | pub rate: bool, 142 | pub stop: bool, 143 | pub utterance_callbacks: bool, 144 | pub voice: bool, 145 | pub get_voice: bool, 146 | pub volume: bool, 147 | } 148 | 149 | impl fmt::Display for Features { 150 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 151 | writeln!(f, "{self:#?}") 152 | } 153 | } 154 | 155 | impl Features { 156 | pub fn new() -> Self { 157 | Self::default() 158 | } 159 | } 160 | 161 | #[derive(Debug, Error)] 162 | pub enum Error { 163 | #[error("IO error: {0}")] 164 | Io(#[from] std::io::Error), 165 | #[error("Value not received")] 166 | NoneError, 167 | #[error("Operation failed")] 168 | OperationFailed, 169 | #[cfg(target_arch = "wasm32")] 170 | #[error("JavaScript error: [0]")] 171 | JavaScriptError(wasm_bindgen::JsValue), 172 | #[cfg(target_os = "linux")] 173 | #[error("Speech Dispatcher error: {0}")] 174 | SpeechDispatcher(#[from] SpeechDispatcherError), 175 | #[cfg(windows)] 176 | #[error("WinRT error")] 177 | WinRt(windows::core::Error), 178 | #[cfg(windows)] 179 | #[error("UTF string conversion failed")] 180 | UtfStringConversionFailed(#[from] FromUtf16Error), 181 | #[error("Unsupported feature")] 182 | UnsupportedFeature, 183 | #[error("Out of range")] 184 | OutOfRange, 185 | #[cfg(target_os = "android")] 186 | #[error("JNI error: [0])]")] 187 | JNI(#[from] jni::errors::Error), 188 | } 189 | 190 | #[clonable] 191 | pub trait Backend: Clone { 192 | fn id(&self) -> Option; 193 | fn supported_features(&self) -> Features; 194 | fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error>; 195 | fn stop(&mut self) -> Result<(), Error>; 196 | fn min_rate(&self) -> f32; 197 | fn max_rate(&self) -> f32; 198 | fn normal_rate(&self) -> f32; 199 | fn get_rate(&self) -> Result; 200 | fn set_rate(&mut self, rate: f32) -> Result<(), Error>; 201 | fn min_pitch(&self) -> f32; 202 | fn max_pitch(&self) -> f32; 203 | fn normal_pitch(&self) -> f32; 204 | fn get_pitch(&self) -> Result; 205 | fn set_pitch(&mut self, pitch: f32) -> Result<(), Error>; 206 | fn min_volume(&self) -> f32; 207 | fn max_volume(&self) -> f32; 208 | fn normal_volume(&self) -> f32; 209 | fn get_volume(&self) -> Result; 210 | fn set_volume(&mut self, volume: f32) -> Result<(), Error>; 211 | fn is_speaking(&self) -> Result; 212 | fn voices(&self) -> Result, Error>; 213 | fn voice(&self) -> Result, Error>; 214 | fn set_voice(&mut self, voice: &Voice) -> Result<(), Error>; 215 | } 216 | 217 | #[derive(Default)] 218 | struct Callbacks { 219 | utterance_begin: Option>, 220 | utterance_end: Option>, 221 | utterance_stop: Option>, 222 | } 223 | 224 | unsafe impl Send for Callbacks {} 225 | 226 | unsafe impl Sync for Callbacks {} 227 | 228 | lazy_static! { 229 | static ref CALLBACKS: Mutex> = { 230 | let m: HashMap = HashMap::new(); 231 | Mutex::new(m) 232 | }; 233 | } 234 | 235 | #[derive(Clone)] 236 | pub struct Tts(Rc>>); 237 | 238 | unsafe impl Send for Tts {} 239 | 240 | unsafe impl Sync for Tts {} 241 | 242 | impl Tts { 243 | /// Create a new `TTS` instance with the specified backend. 244 | pub fn new(backend: Backends) -> Result { 245 | let backend = match backend { 246 | #[cfg(target_os = "linux")] 247 | Backends::SpeechDispatcher => { 248 | let tts = backends::SpeechDispatcher::new()?; 249 | Ok(Tts(Rc::new(RwLock::new(Box::new(tts))))) 250 | } 251 | #[cfg(target_arch = "wasm32")] 252 | Backends::Web => { 253 | let tts = backends::Web::new()?; 254 | Ok(Tts(Rc::new(RwLock::new(Box::new(tts))))) 255 | } 256 | #[cfg(all(windows, feature = "tolk"))] 257 | Backends::Tolk => { 258 | let tts = backends::Tolk::new(); 259 | if let Some(tts) = tts { 260 | Ok(Tts(Rc::new(RwLock::new(Box::new(tts))))) 261 | } else { 262 | Err(Error::NoneError) 263 | } 264 | } 265 | #[cfg(windows)] 266 | Backends::WinRt => { 267 | let tts = backends::WinRt::new()?; 268 | Ok(Tts(Rc::new(RwLock::new(Box::new(tts))))) 269 | } 270 | #[cfg(target_os = "macos")] 271 | Backends::AppKit => Ok(Tts(Rc::new(RwLock::new( 272 | Box::new(backends::AppKit::new()?), 273 | )))), 274 | #[cfg(target_vendor = "apple")] 275 | Backends::AvFoundation => Ok(Tts(Rc::new(RwLock::new(Box::new( 276 | backends::AvFoundation::new()?, 277 | ))))), 278 | #[cfg(target_os = "android")] 279 | Backends::Android => { 280 | let tts = backends::Android::new()?; 281 | Ok(Tts(Rc::new(RwLock::new(Box::new(tts))))) 282 | } 283 | }; 284 | if let Ok(backend) = backend { 285 | if let Some(id) = backend.0.read().unwrap().id() { 286 | let mut callbacks = CALLBACKS.lock().unwrap(); 287 | callbacks.insert(id, Callbacks::default()); 288 | } 289 | Ok(backend) 290 | } else { 291 | backend 292 | } 293 | } 294 | 295 | #[allow(clippy::should_implement_trait)] 296 | pub fn default() -> Result { 297 | #[cfg(target_os = "linux")] 298 | let tts = Tts::new(Backends::SpeechDispatcher); 299 | #[cfg(all(windows, feature = "tolk"))] 300 | let tts = if let Ok(tts) = Tts::new(Backends::Tolk) { 301 | Ok(tts) 302 | } else { 303 | Tts::new(Backends::WinRt) 304 | }; 305 | #[cfg(all(windows, not(feature = "tolk")))] 306 | let tts = Tts::new(Backends::WinRt); 307 | #[cfg(target_arch = "wasm32")] 308 | let tts = Tts::new(Backends::Web); 309 | #[cfg(target_os = "macos")] 310 | let tts = if objc2::available!(macos = 10.14, ..) { 311 | Tts::new(Backends::AvFoundation) 312 | } else { 313 | Tts::new(Backends::AppKit) 314 | }; 315 | #[cfg(all(target_vendor = "apple", not(target_os = "macos")))] 316 | let tts = Tts::new(Backends::AvFoundation); 317 | #[cfg(target_os = "android")] 318 | let tts = Tts::new(Backends::Android); 319 | tts 320 | } 321 | 322 | /// Returns the features supported by this TTS engine 323 | pub fn supported_features(&self) -> Features { 324 | self.0.read().unwrap().supported_features() 325 | } 326 | 327 | /// Speaks the specified text, optionally interrupting current speech. 328 | pub fn speak>( 329 | &mut self, 330 | text: S, 331 | interrupt: bool, 332 | ) -> Result, Error> { 333 | self.0 334 | .write() 335 | .unwrap() 336 | .speak(text.into().as_str(), interrupt) 337 | } 338 | 339 | /// Stops current speech. 340 | pub fn stop(&mut self) -> Result<&Self, Error> { 341 | let Features { stop, .. } = self.supported_features(); 342 | if stop { 343 | self.0.write().unwrap().stop()?; 344 | Ok(self) 345 | } else { 346 | Err(Error::UnsupportedFeature) 347 | } 348 | } 349 | 350 | /// Returns the minimum rate for this speech synthesizer. 351 | pub fn min_rate(&self) -> f32 { 352 | self.0.read().unwrap().min_rate() 353 | } 354 | 355 | /// Returns the maximum rate for this speech synthesizer. 356 | pub fn max_rate(&self) -> f32 { 357 | self.0.read().unwrap().max_rate() 358 | } 359 | 360 | /// Returns the normal rate for this speech synthesizer. 361 | pub fn normal_rate(&self) -> f32 { 362 | self.0.read().unwrap().normal_rate() 363 | } 364 | 365 | /// Gets the current speech rate. 366 | pub fn get_rate(&self) -> Result { 367 | let Features { rate, .. } = self.supported_features(); 368 | if rate { 369 | self.0.read().unwrap().get_rate() 370 | } else { 371 | Err(Error::UnsupportedFeature) 372 | } 373 | } 374 | 375 | /// Sets the desired speech rate. 376 | pub fn set_rate(&mut self, rate: f32) -> Result<&Self, Error> { 377 | let Features { 378 | rate: rate_feature, .. 379 | } = self.supported_features(); 380 | if rate_feature { 381 | let mut backend = self.0.write().unwrap(); 382 | if rate < backend.min_rate() || rate > backend.max_rate() { 383 | Err(Error::OutOfRange) 384 | } else { 385 | backend.set_rate(rate)?; 386 | Ok(self) 387 | } 388 | } else { 389 | Err(Error::UnsupportedFeature) 390 | } 391 | } 392 | 393 | /// Returns the minimum pitch for this speech synthesizer. 394 | pub fn min_pitch(&self) -> f32 { 395 | self.0.read().unwrap().min_pitch() 396 | } 397 | 398 | /// Returns the maximum pitch for this speech synthesizer. 399 | pub fn max_pitch(&self) -> f32 { 400 | self.0.read().unwrap().max_pitch() 401 | } 402 | 403 | /// Returns the normal pitch for this speech synthesizer. 404 | pub fn normal_pitch(&self) -> f32 { 405 | self.0.read().unwrap().normal_pitch() 406 | } 407 | 408 | /// Gets the current speech pitch. 409 | pub fn get_pitch(&self) -> Result { 410 | let Features { pitch, .. } = self.supported_features(); 411 | if pitch { 412 | self.0.read().unwrap().get_pitch() 413 | } else { 414 | Err(Error::UnsupportedFeature) 415 | } 416 | } 417 | 418 | /// Sets the desired speech pitch. 419 | pub fn set_pitch(&mut self, pitch: f32) -> Result<&Self, Error> { 420 | let Features { 421 | pitch: pitch_feature, 422 | .. 423 | } = self.supported_features(); 424 | if pitch_feature { 425 | let mut backend = self.0.write().unwrap(); 426 | if pitch < backend.min_pitch() || pitch > backend.max_pitch() { 427 | Err(Error::OutOfRange) 428 | } else { 429 | backend.set_pitch(pitch)?; 430 | Ok(self) 431 | } 432 | } else { 433 | Err(Error::UnsupportedFeature) 434 | } 435 | } 436 | 437 | /// Returns the minimum volume for this speech synthesizer. 438 | pub fn min_volume(&self) -> f32 { 439 | self.0.read().unwrap().min_volume() 440 | } 441 | 442 | /// Returns the maximum volume for this speech synthesizer. 443 | pub fn max_volume(&self) -> f32 { 444 | self.0.read().unwrap().max_volume() 445 | } 446 | 447 | /// Returns the normal volume for this speech synthesizer. 448 | pub fn normal_volume(&self) -> f32 { 449 | self.0.read().unwrap().normal_volume() 450 | } 451 | 452 | /// Gets the current speech volume. 453 | pub fn get_volume(&self) -> Result { 454 | let Features { volume, .. } = self.supported_features(); 455 | if volume { 456 | self.0.read().unwrap().get_volume() 457 | } else { 458 | Err(Error::UnsupportedFeature) 459 | } 460 | } 461 | 462 | /// Sets the desired speech volume. 463 | pub fn set_volume(&mut self, volume: f32) -> Result<&Self, Error> { 464 | let Features { 465 | volume: volume_feature, 466 | .. 467 | } = self.supported_features(); 468 | if volume_feature { 469 | let mut backend = self.0.write().unwrap(); 470 | if volume < backend.min_volume() || volume > backend.max_volume() { 471 | Err(Error::OutOfRange) 472 | } else { 473 | backend.set_volume(volume)?; 474 | Ok(self) 475 | } 476 | } else { 477 | Err(Error::UnsupportedFeature) 478 | } 479 | } 480 | 481 | /// Returns whether this speech synthesizer is speaking. 482 | pub fn is_speaking(&self) -> Result { 483 | let Features { is_speaking, .. } = self.supported_features(); 484 | if is_speaking { 485 | self.0.read().unwrap().is_speaking() 486 | } else { 487 | Err(Error::UnsupportedFeature) 488 | } 489 | } 490 | 491 | /// Returns list of available voices. 492 | pub fn voices(&self) -> Result, Error> { 493 | let Features { voice, .. } = self.supported_features(); 494 | if voice { 495 | self.0.read().unwrap().voices() 496 | } else { 497 | Err(Error::UnsupportedFeature) 498 | } 499 | } 500 | 501 | /// Return the current speaking voice. 502 | pub fn voice(&self) -> Result, Error> { 503 | let Features { get_voice, .. } = self.supported_features(); 504 | if get_voice { 505 | self.0.read().unwrap().voice() 506 | } else { 507 | Err(Error::UnsupportedFeature) 508 | } 509 | } 510 | 511 | /// Set speaking voice. 512 | pub fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { 513 | let Features { 514 | voice: voice_feature, 515 | .. 516 | } = self.supported_features(); 517 | if voice_feature { 518 | self.0.write().unwrap().set_voice(voice) 519 | } else { 520 | Err(Error::UnsupportedFeature) 521 | } 522 | } 523 | 524 | /// Called when this speech synthesizer begins speaking an utterance. 525 | pub fn on_utterance_begin( 526 | &self, 527 | callback: Option>, 528 | ) -> Result<(), Error> { 529 | let Features { 530 | utterance_callbacks, 531 | .. 532 | } = self.supported_features(); 533 | if utterance_callbacks { 534 | let mut callbacks = CALLBACKS.lock().unwrap(); 535 | let id = self.0.read().unwrap().id().unwrap(); 536 | let callbacks = callbacks.get_mut(&id).unwrap(); 537 | callbacks.utterance_begin = callback; 538 | Ok(()) 539 | } else { 540 | Err(Error::UnsupportedFeature) 541 | } 542 | } 543 | 544 | /// Called when this speech synthesizer finishes speaking an utterance. 545 | pub fn on_utterance_end( 546 | &self, 547 | callback: Option>, 548 | ) -> Result<(), Error> { 549 | let Features { 550 | utterance_callbacks, 551 | .. 552 | } = self.supported_features(); 553 | if utterance_callbacks { 554 | let mut callbacks = CALLBACKS.lock().unwrap(); 555 | let id = self.0.read().unwrap().id().unwrap(); 556 | let callbacks = callbacks.get_mut(&id).unwrap(); 557 | callbacks.utterance_end = callback; 558 | Ok(()) 559 | } else { 560 | Err(Error::UnsupportedFeature) 561 | } 562 | } 563 | 564 | /// Called when this speech synthesizer is stopped and still has utterances in its queue. 565 | pub fn on_utterance_stop( 566 | &self, 567 | callback: Option>, 568 | ) -> Result<(), Error> { 569 | let Features { 570 | utterance_callbacks, 571 | .. 572 | } = self.supported_features(); 573 | if utterance_callbacks { 574 | let mut callbacks = CALLBACKS.lock().unwrap(); 575 | let id = self.0.read().unwrap().id().unwrap(); 576 | let callbacks = callbacks.get_mut(&id).unwrap(); 577 | callbacks.utterance_stop = callback; 578 | Ok(()) 579 | } else { 580 | Err(Error::UnsupportedFeature) 581 | } 582 | } 583 | 584 | /* 585 | * Returns `true` if a screen reader is available to provide speech. 586 | */ 587 | #[allow(unreachable_code)] 588 | pub fn screen_reader_available() -> bool { 589 | #[cfg(target_os = "windows")] 590 | { 591 | #[cfg(feature = "tolk")] 592 | { 593 | let tolk = Tolk::new(); 594 | return tolk.detect_screen_reader().is_some(); 595 | } 596 | #[cfg(not(feature = "tolk"))] 597 | return false; 598 | } 599 | false 600 | } 601 | } 602 | 603 | impl Drop for Tts { 604 | fn drop(&mut self) { 605 | if Rc::strong_count(&self.0) <= 1 { 606 | if let Some(id) = self.0.read().unwrap().id() { 607 | let mut callbacks = CALLBACKS.lock().unwrap(); 608 | callbacks.remove(&id); 609 | } 610 | } 611 | } 612 | } 613 | 614 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 615 | pub enum Gender { 616 | Male, 617 | Female, 618 | } 619 | 620 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 621 | pub struct Voice { 622 | pub(crate) id: String, 623 | pub(crate) name: String, 624 | pub(crate) gender: Option, 625 | pub(crate) language: LanguageTag, 626 | } 627 | 628 | impl Voice { 629 | pub fn id(&self) -> String { 630 | self.id.clone() 631 | } 632 | 633 | pub fn name(&self) -> String { 634 | self.name.clone() 635 | } 636 | 637 | pub fn gender(&self) -> Option { 638 | self.gender 639 | } 640 | 641 | pub fn language(&self) -> LanguageTag { 642 | self.language.clone() 643 | } 644 | } 645 | --------------------------------------------------------------------------------