mirror of
https://github.com/ndarilek/tts-rs.git
synced 2024-11-14 17:39:37 +00:00
Compare commits
355 Commits
Author | SHA1 | Date | |
---|---|---|---|
b31efe752d | |||
ae7bf7554c | |||
3bc16f0c6f | |||
3c8ae0ae42 | |||
07edc20861 | |||
96a5209a9f | |||
20b18949e2 | |||
f29de0aede | |||
9e1476fd36 | |||
3032fe0fb3 | |||
edd09c24e7 | |||
b7b4e7dc85 | |||
f593340051 | |||
|
12d8e1f532 | ||
|
2a81dc9b70 | ||
|
a0c6cbaf6a | ||
5c528f1d8e | |||
9fb8107acf | |||
8dabcc99c4 | |||
b369fb5614 | |||
e6e1cd49bf | |||
e2edc18e6e | |||
3eba940a22 | |||
7e761e1267 | |||
bf8eb07866 | |||
f5be2b7657 | |||
b7e7ed46dd | |||
69eebf2ffa | |||
6c6089daf9 | |||
|
c874607afe | ||
2667d4e943 | |||
8b506a89e0 | |||
dcaf5b914d | |||
359b1c8053 | |||
527b4cd61e | |||
97fa370dec | |||
915673eec6 | |||
cf72bad59a | |||
246e587f2d | |||
d65d79f8fb | |||
c339d2bee3 | |||
daaead1dc3 | |||
d547d84af0 | |||
f6766ec633 | |||
8102820f86 | |||
3c9a78a953 | |||
5470b9557d | |||
6770a2ed58 | |||
61d84a2120 | |||
7a91a1e827 | |||
e19e5ef0b7 | |||
3e4299d0e6 | |||
d42d20189a | |||
22ae0ef5a3 | |||
f5716c48f5 | |||
eb1d13976a | |||
259549e21d | |||
3679ad6153 | |||
|
94615a254a | ||
|
ddf96c10aa | ||
|
3fdd452646 | ||
b4f48fa439 | |||
919bc4249a | |||
79e59d551f | |||
652367fa8a | |||
3c38c783b0 | |||
1eae827ed1 | |||
f404e180e4 | |||
7cf80fb64d | |||
b50c5b6b93 | |||
748f07138d | |||
|
15f28c9af4 | ||
b3d2b788f7 | |||
5feb8e3186 | |||
238d7e2cb3 | |||
507d0b5418 | |||
10ac1021ee | |||
323f129b7b | |||
9b4ae761a0 | |||
40f682080d | |||
40e28876b2 | |||
4079f4b3c4 | |||
4283623723 | |||
569bb160b8 | |||
4d01717e75 | |||
da19d5f16c | |||
822f770ab8 | |||
9bd767629a | |||
2b4251f6fa | |||
219cfbbe00 | |||
264af78c58 | |||
e3542abd7c | |||
55c0fbbd2b | |||
a0945d7ebb | |||
c627583928 | |||
ec6d1f74a1 | |||
b9aa36cb3b | |||
e699f7e5e5 | |||
3f9e7c22db | |||
e4c6f6f23a | |||
d4b913908c | |||
b1f60811bf | |||
51cd84a6cd | |||
142f2e6b3a | |||
1e55c43153 | |||
e56a0da2e5 | |||
55f841d887 | |||
c222c087b2 | |||
6057d9c968 | |||
|
88f4598ec6 | ||
acecb1f362 | |||
fd9f5ae60a | |||
b435c89239 | |||
3366f93e2b | |||
539003205e | |||
5c9c649505 | |||
f275e506df | |||
ef0a78c745 | |||
cc8fd91c86 | |||
888e6a3dfa | |||
5944794980 | |||
31309553bb | |||
00bd5e62ff | |||
90f7dae6a1 | |||
f310306508 | |||
9f8b670fe0 | |||
cd2216390c | |||
4466923620 | |||
660072809d | |||
1e1c04d4e5 | |||
050b97fde1 | |||
dc00aa427f | |||
1f466275cf | |||
9066d2b005 | |||
cdc225418e | |||
ad785b6536 | |||
89708d9ef1 | |||
e3f9ebe431 | |||
|
114fb55fc9 | ||
2ea472e196 | |||
|
5331bc8daf | ||
|
e20170583d | ||
|
9ed03753c2 | ||
|
bed6cfa206 | ||
|
5e9c98b063 | ||
|
ee8ec97ab4 | ||
d24d1a6a15 | |||
94417b5351 | |||
89fd14d957 | |||
47e164a0c8 | |||
57ffbf0e4f | |||
119678ae55 | |||
d5bdb9f498 | |||
562489e5af | |||
c12f328cf2 | |||
f8dbc04c36 | |||
a703e790ec | |||
92538fbdb8 | |||
dc3129b79c | |||
c4038149a8 | |||
ca7789f157 | |||
d85d56c3ee | |||
d67bf8344a | |||
8f5f58028a | |||
86b2e07f15 | |||
4088eb12a1 | |||
7b8da53d81 | |||
e4b53d17aa | |||
316b1bceec | |||
26d06fc635 | |||
a879b3dca3 | |||
d5a692008a | |||
f7239366f0 | |||
debab7de17 | |||
1011704b82 | |||
d9639c049b | |||
6dbf9b7ddc | |||
336c266ed4 | |||
57f91105ec | |||
ef96042b12 | |||
acccdfeada | |||
153075ebab | |||
25f8211661 | |||
fb7f1dddfc | |||
50528ce2d1 | |||
8c2aae7afd | |||
ed2d2e76c3 | |||
45255a8049 | |||
c65c0022d8 | |||
bd8e2ee20a | |||
00485d6cd8 | |||
cdfb7ddb77 | |||
290eb06d02 | |||
e91637a67c | |||
81eba99594 | |||
1f510120a5 | |||
1d075f7ece | |||
a22ee53727 | |||
2bd324b08b | |||
8ba1f91617 | |||
c9279804b7 | |||
6664ca89e3 | |||
2fd98c0a52 | |||
6784bb8861 | |||
c21d4a6a38 | |||
00a16c5dd5 | |||
d9ca83ca15 | |||
42879dfa1f | |||
296fa89f5d | |||
3e1f5af61a | |||
15b7b33ed3 | |||
22cff2ddd1 | |||
06eb32b6d4 | |||
69af3465b3 | |||
699d0d23e9 | |||
7eb74729fc | |||
d806c44c76 | |||
adfb2146ac | |||
914a7a1972 | |||
cf39be85af | |||
cee5777556 | |||
a01fd93502 | |||
8d6f40b1a5 | |||
0ea46b29b2 | |||
c92b67127c | |||
733b17fe2c | |||
440154502b | |||
2120de8756 | |||
e1c2171833 | |||
22ee9863d6 | |||
5634fdb393 | |||
1ac0b91981 | |||
32f57d8578 | |||
f58f875fdf | |||
84926ea110 | |||
da8260cba8 | |||
965bea0adf | |||
fc20431916 | |||
cb91760468 | |||
187cd71eeb | |||
5849e340c9 | |||
6d17447350 | |||
22007fbf79 | |||
d8f2b3fb00 | |||
a905439d9c | |||
49e8c0e5dc | |||
ad67682235 | |||
184becfd1a | |||
0bdf0fcfd3 | |||
f4952ad132 | |||
728c409e25 | |||
669c94af36 | |||
fa2903606e | |||
9fb4f5e71e | |||
34db699972 | |||
80d51e1bff | |||
f3705a1856 | |||
2f0ced4eaf | |||
d5f92565e5 | |||
3224cbdf5a | |||
be96aacd7a | |||
d97796fff7 | |||
f37133841a | |||
3157162192 | |||
2c73c75e00 | |||
b6ef11b60f | |||
10010f9bc9 | |||
3500e88117 | |||
1cbeab6ea9 | |||
590d6369fb | |||
80fa5d4583 | |||
6a706f36ab | |||
e1791c7046 | |||
031e0ff23f | |||
204cd50935 | |||
6b74afe503 | |||
4e157b6fb5 | |||
df4adc81a7 | |||
289a35dc83 | |||
9c98026978 | |||
d3e05b5a7a | |||
5a9c96508f | |||
2343523bb6 | |||
29c0a8463e | |||
cf0ad2221e | |||
551bb1292e | |||
565aa6d654 | |||
efdb274eb4 | |||
5feede0b8f | |||
1d48cb93d7 | |||
0bbda0a90f | |||
e66b8403aa | |||
a281d74e5c | |||
5d1625e5e2 | |||
ca186671b4 | |||
1be226df8a | |||
baa442f136 | |||
51837a51bf | |||
fa216a534e | |||
6eb03fb1a3 | |||
88ec7db075 | |||
ba90cd66ba | |||
0c13c43a77 | |||
724dd1214f | |||
6f12974ce4 | |||
8c783205c3 | |||
174011bbb4 | |||
|
d2c42d97f5 | ||
|
3294a82485 | ||
|
e19eb56169 | ||
|
f7297e18fd | ||
|
f78aed211f | ||
|
008662c940 | ||
|
8c8dc0ae9f | ||
c2bbc5ac04 | |||
dbac8a3fe0 | |||
|
47cbb80595 | ||
ace5d2fd1f | |||
589c613bbe | |||
1f22843086 | |||
2c70f77a15 | |||
96e5d21e24 | |||
a22242af50 | |||
532d5d9b58 | |||
251fb8d8c1 | |||
bd57075d53 | |||
36a12597de | |||
c5524113ff | |||
6788277a4d | |||
61522610cd | |||
f5f11b7cdf | |||
017aa8863b | |||
6b023c3071 | |||
4816ec575c | |||
d6508edd12 | |||
|
97f1de5724 | ||
|
335ac710a6 | ||
|
b238c8c938 | ||
|
1b8809aaeb | ||
|
0fb6c62d83 | ||
|
6ed94686f3 | ||
|
5b0d1b6621 | ||
14a721c837 | |||
c8fd02b448 | |||
03ea2602bc | |||
dac58539c9 | |||
0d61dc258f | |||
2cfd2ea09e | |||
d3ca27c707 | |||
81b23330e9 | |||
665013fdff | |||
6c091f3284 | |||
d3ffd5078f | |||
1507527175 | |||
951e31b284 |
54
.github/workflows/release.yml
vendored
54
.github/workflows/release.yml
vendored
|
@ -6,64 +6,16 @@ on:
|
||||||
- "v*"
|
- "v*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build_linux:
|
|
||||||
name: Build Linux
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libspeechd-dev
|
|
||||||
cargo build --release
|
|
||||||
rustup target add wasm32-unknown-unknown
|
|
||||||
cargo build --release --target wasm32-unknown-unknown
|
|
||||||
|
|
||||||
build_windows:
|
|
||||||
name: Build Windows
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- run: |
|
|
||||||
choco install -y llvm
|
|
||||||
cargo build --release
|
|
||||||
|
|
||||||
build_macos:
|
|
||||||
name: Build MacOS
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- run: |
|
|
||||||
cargo build --release
|
|
||||||
rustup target add aarch64-apple-ios x86_64-apple-ios
|
|
||||||
cargo install cargo-lipo
|
|
||||||
cargo lipo --release
|
|
||||||
|
|
||||||
publish_winrt_bindings:
|
|
||||||
name: Publish winrt_bindings
|
|
||||||
runs-on: windows-latest
|
|
||||||
needs: [build_windows]
|
|
||||||
env:
|
|
||||||
CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- run: |
|
|
||||||
choco install -y llvm
|
|
||||||
cargo login $CARGO_TOKEN
|
|
||||||
cd winrt_bindings
|
|
||||||
cargo package
|
|
||||||
cargo publish || true
|
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish
|
name: Publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
needs: [build_linux, build_windows, build_macos]
|
|
||||||
env:
|
env:
|
||||||
CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }}
|
CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- run: |
|
- run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libspeechd-dev
|
sudo apt-get install -y libspeechd-dev
|
||||||
cargo login $CARGO_TOKEN
|
cargo login $CARGO_TOKEN
|
||||||
|
rustup toolchain install stable
|
||||||
cargo publish
|
cargo publish
|
||||||
|
|
69
.github/workflows/test.yml
vendored
69
.github/workflows/test.yml
vendored
|
@ -5,35 +5,58 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
check_formatting:
|
||||||
build_linux:
|
name: Check Formatting
|
||||||
name: Build Linux
|
runs-on: ubuntu-22.04
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
|
- run: |
|
||||||
|
rustup toolchain install stable
|
||||||
|
cargo fmt --all --check
|
||||||
|
cd examples/web
|
||||||
|
cargo fmt --all --check
|
||||||
|
|
||||||
|
check:
|
||||||
|
name: Check
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest, ubuntu-22.04, macos-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: sudo apt-get update; sudo apt-get install -y libspeechd-dev
|
||||||
|
if: ${{ runner.os == 'Linux' }}
|
||||||
|
- run: |
|
||||||
|
rustup toolchain install stable
|
||||||
|
cargo clippy --all-targets
|
||||||
|
|
||||||
|
check_web:
|
||||||
|
name: Check Web
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
- run: |
|
- run: |
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libspeechd-dev
|
|
||||||
cargo build --release
|
|
||||||
rustup target add wasm32-unknown-unknown
|
rustup target add wasm32-unknown-unknown
|
||||||
cargo build --release --target wasm32-unknown-unknown
|
rustup toolchain install stable
|
||||||
|
cargo clippy --all-targets --target wasm32-unknown-unknown
|
||||||
|
|
||||||
build_windows:
|
check_android:
|
||||||
name: Build Windows
|
name: Check Android
|
||||||
runs-on: windows-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- run: |
|
- run: |
|
||||||
choco install -y llvm
|
rustup target add aarch64-linux-android
|
||||||
cargo build --release
|
rustup toolchain install stable
|
||||||
|
cargo clippy --all-targets --target aarch64-linux-android
|
||||||
|
|
||||||
build_macos:
|
check_web_example:
|
||||||
name: Build MacOS
|
name: Check Web Example
|
||||||
runs-on: macos-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- run: |
|
- run: |
|
||||||
cargo build --release
|
rustup target add wasm32-unknown-unknown
|
||||||
rustup target add aarch64-apple-ios x86_64-apple-ios
|
rustup toolchain install stable
|
||||||
cargo install cargo-lipo
|
cd examples/web
|
||||||
cargo lipo --release
|
cargo build --target wasm32-unknown-unknown
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
target
|
target
|
||||||
|
*.dll
|
55
Cargo.toml
55
Cargo.toml
|
@ -1,36 +1,71 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tts"
|
name = "tts"
|
||||||
version = "0.6.0"
|
version = "0.26.3"
|
||||||
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
|
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
|
||||||
repository = "https://github.com/ndarilek/tts-rs"
|
repository = "https://github.com/ndarilek/tts-rs"
|
||||||
description = "High-level Text-To-Speech (TTS) interface"
|
description = "High-level Text-To-Speech (TTS) interface"
|
||||||
|
documentation = "https://docs.rs/tts"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
exclude = ["*.cfg", "*.yml"]
|
exclude = ["*.cfg", "*.yml"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["lib", "staticlib"]
|
crate-type = ["lib", "cdylib", "staticlib"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
speech_dispatcher_0_9 = ["speech-dispatcher/0_9"]
|
||||||
|
speech_dispatcher_0_10 = ["speech-dispatcher/0_10"]
|
||||||
|
speech_dispatcher_0_11 = ["speech-dispatcher/0_11"]
|
||||||
|
default = ["speech_dispatcher_0_11"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
dyn-clonable = "0.9"
|
||||||
|
oxilangtag = "0.1"
|
||||||
|
lazy_static = "1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
serde = { version = "1", optional = true, features = ["derive"] }
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.7"
|
env_logger = "0.11"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
tolk = "0.2"
|
tolk = { version = "0.5", optional = true }
|
||||||
winrt = "0.7"
|
windows = { version = "0.58", features = [
|
||||||
tts_winrt_bindings = { version = "0.1", path="winrt_bindings" }
|
"Foundation",
|
||||||
|
"Foundation_Collections",
|
||||||
|
"Media_Core",
|
||||||
|
"Media_Playback",
|
||||||
|
"Media_SpeechSynthesis",
|
||||||
|
"Storage_Streams",
|
||||||
|
] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
speech-dispatcher = "0.4"
|
speech-dispatcher = { version = "0.16", default-features = false }
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
||||||
cocoa-foundation = "0.1"
|
cocoa-foundation = "0.1"
|
||||||
|
core-foundation = "0.9"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
objc = "0.2"
|
objc = { version = "0.2", features = ["exception"] }
|
||||||
|
|
||||||
[target.wasm32-unknown-unknown.dependencies]
|
[target.wasm32-unknown-unknown.dependencies]
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
web-sys = { version = "0.3", features = ["SpeechSynthesis", "SpeechSynthesisUtterance", "Window", ] }
|
web-sys = { version = "0.3", features = [
|
||||||
|
"EventTarget",
|
||||||
|
"SpeechSynthesis",
|
||||||
|
"SpeechSynthesisErrorCode",
|
||||||
|
"SpeechSynthesisErrorEvent",
|
||||||
|
"SpeechSynthesisEvent",
|
||||||
|
"SpeechSynthesisUtterance",
|
||||||
|
"SpeechSynthesisVoice",
|
||||||
|
"Window",
|
||||||
|
] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os="android")'.dependencies]
|
||||||
|
jni = "0.21"
|
||||||
|
ndk-context = "0.1"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
no-default-features = true
|
||||||
|
features = ["speech_dispatcher_0_11"]
|
||||||
|
|
33
Makefile.toml
Normal file
33
Makefile.toml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
[tasks.build-android-example]
|
||||||
|
script = [
|
||||||
|
"cd examples/android",
|
||||||
|
"./gradlew assembleDebug",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.run-android-example]
|
||||||
|
script = [
|
||||||
|
"cd examples/android",
|
||||||
|
"./gradlew runDebug",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tasks.log-android]
|
||||||
|
command = "adb"
|
||||||
|
args = ["logcat", "RustStdoutStderr:D", "*:S"]
|
||||||
|
|
||||||
|
[tasks.install-trunk]
|
||||||
|
install_crate = { crate_name = "trunk", binary = "trunk", test_arg = "--help" }
|
||||||
|
|
||||||
|
[tasks.install-wasm-bindgen-cli]
|
||||||
|
install_crate = { crate_name = "wasm-bindgen-cli", binary = "wasm-bindgen", test_arg = "--help" }
|
||||||
|
|
||||||
|
[tasks.build-web-example]
|
||||||
|
dependencies = ["install-trunk", "install-wasm-bindgen-cli"]
|
||||||
|
cwd = "examples/web"
|
||||||
|
command = "trunk"
|
||||||
|
args = ["build"]
|
||||||
|
|
||||||
|
[tasks.run-web-example]
|
||||||
|
dependencies = ["install-trunk", "install-wasm-bindgen-cli"]
|
||||||
|
cwd = "examples/web"
|
||||||
|
command = "trunk"
|
||||||
|
args = ["serve"]
|
17
README.md
17
README.md
|
@ -3,10 +3,23 @@
|
||||||
This library provides a high-level Text-To-Speech (TTS) interface supporting various backends. Currently supported backends are:
|
This library provides a high-level Text-To-Speech (TTS) interface supporting various backends. Currently supported backends are:
|
||||||
|
|
||||||
* Windows
|
* Windows
|
||||||
* Screen readers/SAPI via Tolk
|
* Screen readers/SAPI via Tolk (requires `tolk` Cargo feature)
|
||||||
* WinRT
|
* WinRT
|
||||||
* Linux via [Speech Dispatcher](https://freebsoft.org/speechd)
|
* Linux via [Speech Dispatcher](https://freebsoft.org/speechd)
|
||||||
* MacOS
|
* MacOS/iOS
|
||||||
* AppKit on MacOS 10.13 and below
|
* AppKit on MacOS 10.13 and below
|
||||||
* AVFoundation on MacOS 10.14 and above, and iOS
|
* AVFoundation on MacOS 10.14 and above, and iOS
|
||||||
|
* Android
|
||||||
* WebAssembly
|
* WebAssembly
|
||||||
|
|
||||||
|
## Android Setup
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
* Set up _Cargo.toml_ as the example does. Be sure to depend on `ndk-glue`.
|
||||||
|
* Place _Bridge.java_ appropriately in your app. This is needed to support various Android TTS callbacks.
|
||||||
|
* 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`.
|
||||||
|
* * 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.
|
||||||
|
* 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_.
|
||||||
|
|
||||||
|
And I think that should about do it. Good luck!
|
16
build.rs
16
build.rs
|
@ -1,15 +1,11 @@
|
||||||
// Copyright 2013-2015 The Servo Project Developers. See the COPYRIGHT
|
|
||||||
// file at the top-level directory of this distribution.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
||||||
// option. This file may not be copied, modified, or distributed
|
|
||||||
// except according to those terms.
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
if std::env::var("TARGET").unwrap().contains("-apple") {
|
if std::env::var("TARGET").unwrap().contains("-apple") {
|
||||||
println!("cargo:rustc-link-lib=framework=AppKit");
|
|
||||||
println!("cargo:rustc-link-lib=framework=AVFoundation");
|
println!("cargo:rustc-link-lib=framework=AVFoundation");
|
||||||
|
if !std::env::var("CARGO_CFG_TARGET_OS")
|
||||||
|
.unwrap()
|
||||||
|
.contains("ios")
|
||||||
|
{
|
||||||
|
println!("cargo:rustc-link-lib=framework=AppKit");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
39
examples/99bottles.rs
Normal file
39
examples/99bottles.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
use std::io;
|
||||||
|
use std::{thread, time};
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use cocoa_foundation::base::id;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use cocoa_foundation::foundation::NSRunLoop;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use objc::{msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
use tts::*;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Error> {
|
||||||
|
env_logger::init();
|
||||||
|
let mut tts = Tts::default()?;
|
||||||
|
let mut bottles = 99;
|
||||||
|
while bottles > 0 {
|
||||||
|
tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?;
|
||||||
|
tts.speak(format!("{} bottles of beer,", bottles), false)?;
|
||||||
|
tts.speak("Take one down, pass it around", false)?;
|
||||||
|
tts.speak("Give us a bit to drink this...", false)?;
|
||||||
|
let time = time::Duration::from_secs(15);
|
||||||
|
thread::sleep(time);
|
||||||
|
bottles -= 1;
|
||||||
|
tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?;
|
||||||
|
}
|
||||||
|
let mut _input = String::new();
|
||||||
|
// The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context.
|
||||||
|
// It shouldn't be needed in an app or game that almost certainly has one already.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let run_loop: id = unsafe { NSRunLoop::currentRunLoop() };
|
||||||
|
unsafe {
|
||||||
|
let _: () = msg_send![run_loop, run];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
io::stdin().read_line(&mut _input)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
16
examples/android/.gitignore
vendored
Normal file
16
examples/android/.gitignore
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
|
Cargo.lock
|
3
examples/android/.idea/.gitignore
vendored
Normal file
3
examples/android/.idea/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
6
examples/android/.idea/compiler.xml
Normal file
6
examples/android/.idea/compiler.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="1.6" />
|
||||||
|
</component>
|
||||||
|
</project>
|
21
examples/android/.idea/gradle.xml
Normal file
21
examples/android/.idea/gradle.xml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="PLATFORM" />
|
||||||
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="11" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
<option name="resolveModulePerSourceSet" value="false" />
|
||||||
|
<option name="useQualifiedModuleNames" value="true" />
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
25
examples/android/.idea/jarRepositories.xml
Normal file
25
examples/android/.idea/jarRepositories.xml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RemoteRepositoriesConfiguration">
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Maven Central repository" />
|
||||||
|
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="jboss.community" />
|
||||||
|
<option name="name" value="JBoss Community repository" />
|
||||||
|
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="BintrayJCenter" />
|
||||||
|
<option name="name" value="BintrayJCenter" />
|
||||||
|
<option name="url" value="https://jcenter.bintray.com/" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="Google" />
|
||||||
|
<option name="name" value="Google" />
|
||||||
|
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||||
|
</remote-repository>
|
||||||
|
</component>
|
||||||
|
</project>
|
9
examples/android/.idea/misc.xml
Normal file
9
examples/android/.idea/misc.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
6
examples/android/.idea/vcs.xml
Normal file
6
examples/android/.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
1
examples/android/app/.gitignore
vendored
Normal file
1
examples/android/app/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
56
examples/android/app/build.gradle
Normal file
56
examples/android/app/build.gradle
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "org.mozilla.rust-android-gradle.rust-android"
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "rs.tts"
|
||||||
|
compileSdkVersion 33
|
||||||
|
ndkVersion "25.1.8937393"
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "rs.tts"
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion 33
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "androidx.core:core-ktx:1.2.0"
|
||||||
|
implementation "androidx.annotation:annotation:1.1.0"
|
||||||
|
implementation "com.google.android.material:material:1.1.0"
|
||||||
|
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: "org.mozilla.rust-android-gradle.rust-android"
|
||||||
|
|
||||||
|
cargo {
|
||||||
|
module = "."
|
||||||
|
libname = "tts"
|
||||||
|
targets = ["arm", "x86"]
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.whenTaskAdded { task ->
|
||||||
|
if ((task.name == 'javaPreCompileDebug' || task.name == 'javaPreCompileRelease')) {
|
||||||
|
task.dependsOn "cargoBuild"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
project.afterEvaluate {
|
||||||
|
android.applicationVariants.all { variant ->
|
||||||
|
task "run${variant.name.capitalize()}"(type: Exec, dependsOn: "install${variant.name.capitalize()}", group: "run") {
|
||||||
|
commandLine = ["adb", "shell", "monkey", "-p", variant.applicationId + " 1"]
|
||||||
|
doLast {
|
||||||
|
println "Launching ${variant.applicationId}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
examples/android/app/proguard-rules.pro
vendored
Normal file
21
examples/android/app/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
1
examples/android/app/src/main/.gitignore
vendored
Normal file
1
examples/android/app/src/main/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
jniLibs
|
13
examples/android/app/src/main/AndroidManifest.xml
Normal file
13
examples/android/app/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application android:allowBackup="true" android:label="@string/app_name">
|
||||||
|
<activity android:name=".MainActivity" android:exported="true">
|
||||||
|
<meta-data android:name="android.app.lib_name" android:value="hello_world" />
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
24
examples/android/app/src/main/java/rs/tts/Bridge.java
Normal file
24
examples/android/app/src/main/java/rs/tts/Bridge.java
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package rs.tts;
|
||||||
|
|
||||||
|
import android.speech.tts.TextToSpeech;
|
||||||
|
import android.speech.tts.UtteranceProgressListener;
|
||||||
|
|
||||||
|
@androidx.annotation.Keep
|
||||||
|
public class Bridge extends UtteranceProgressListener implements TextToSpeech.OnInitListener {
|
||||||
|
public int backendId;
|
||||||
|
|
||||||
|
public Bridge(int backendId) {
|
||||||
|
this.backendId = backendId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public native void onInit(int status);
|
||||||
|
|
||||||
|
public native void onStart(String utteranceId);
|
||||||
|
|
||||||
|
public native void onStop(String utteranceId, Boolean interrupted);
|
||||||
|
|
||||||
|
public native void onDone(String utteranceId);
|
||||||
|
|
||||||
|
public native void onError(String utteranceId) ;
|
||||||
|
|
||||||
|
}
|
11
examples/android/app/src/main/java/rs/tts/MainActivity.kt
Normal file
11
examples/android/app/src/main/java/rs/tts/MainActivity.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package rs.tts
|
||||||
|
|
||||||
|
import android.app.NativeActivity
|
||||||
|
|
||||||
|
class MainActivity : NativeActivity() {
|
||||||
|
companion object {
|
||||||
|
init {
|
||||||
|
System.loadLibrary("hello_world")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
examples/android/app/src/main/res/values/strings.xml
Normal file
3
examples/android/app/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">TTS-RS</string>
|
||||||
|
</resources>
|
29
examples/android/build.gradle
Normal file
29
examples/android/build.gradle
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url "https://plugins.gradle.org/m2/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "com.android.application" version "7.3.0" apply false
|
||||||
|
id "com.android.library" version "7.3.0" apply false
|
||||||
|
id "org.jetbrains.kotlin.android" version "1.7.21" apply false
|
||||||
|
id "org.mozilla.rust-android-gradle.rust-android" version "0.9.3" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
14
examples/android/cargo.toml
Normal file
14
examples/android/cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "hello_world"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["dylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ndk-glue = "0.7"
|
||||||
|
tts = { path = "../.." }
|
21
examples/android/gradle.properties
Normal file
21
examples/android/gradle.properties
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app"s APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Automatically convert third-party libraries to use AndroidX
|
||||||
|
android.enableJetifier=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
BIN
examples/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
examples/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
examples/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
examples/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#Mon Dec 28 17:32:22 CST 2020
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
172
examples/android/gradlew
vendored
Executable file
172
examples/android/gradlew
vendored
Executable file
|
@ -0,0 +1,172 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=$(save "$@")
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||||
|
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
84
examples/android/gradlew.bat
vendored
Normal file
84
examples/android/gradlew.bat
vendored
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS=
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
8
examples/android/settings.gradle
Normal file
8
examples/android/settings.gradle
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
include ":app"
|
70
examples/android/src/lib.rs
Normal file
70
examples/android/src/lib.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
use tts::*;
|
||||||
|
|
||||||
|
// The `loop {}` below only simulates an app loop.
|
||||||
|
// Without it, the `TTS` instance gets dropped before callbacks can run.
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
fn run() -> Result<(), Error> {
|
||||||
|
let mut tts = Tts::default()?;
|
||||||
|
let Features {
|
||||||
|
utterance_callbacks,
|
||||||
|
..
|
||||||
|
} = tts.supported_features();
|
||||||
|
if utterance_callbacks {
|
||||||
|
tts.on_utterance_begin(Some(Box::new(|utterance| {
|
||||||
|
println!("Started speaking {:?}", utterance)
|
||||||
|
})))?;
|
||||||
|
tts.on_utterance_end(Some(Box::new(|utterance| {
|
||||||
|
println!("Finished speaking {:?}", utterance)
|
||||||
|
})))?;
|
||||||
|
tts.on_utterance_stop(Some(Box::new(|utterance| {
|
||||||
|
println!("Stopped speaking {:?}", utterance)
|
||||||
|
})))?;
|
||||||
|
}
|
||||||
|
let Features { is_speaking, .. } = tts.supported_features();
|
||||||
|
if is_speaking {
|
||||||
|
println!("Are we speaking? {}", tts.is_speaking()?);
|
||||||
|
}
|
||||||
|
tts.speak("Hello, world.", false)?;
|
||||||
|
let Features { rate, .. } = tts.supported_features();
|
||||||
|
if rate {
|
||||||
|
let original_rate = tts.get_rate()?;
|
||||||
|
tts.speak(format!("Current rate: {}", original_rate), false)?;
|
||||||
|
tts.set_rate(tts.max_rate())?;
|
||||||
|
tts.speak("This is very fast.", false)?;
|
||||||
|
tts.set_rate(tts.min_rate())?;
|
||||||
|
tts.speak("This is very slow.", false)?;
|
||||||
|
tts.set_rate(tts.normal_rate())?;
|
||||||
|
tts.speak("This is the normal rate.", false)?;
|
||||||
|
tts.set_rate(original_rate)?;
|
||||||
|
}
|
||||||
|
let Features { pitch, .. } = tts.supported_features();
|
||||||
|
if pitch {
|
||||||
|
let original_pitch = tts.get_pitch()?;
|
||||||
|
tts.set_pitch(tts.max_pitch())?;
|
||||||
|
tts.speak("This is high-pitch.", false)?;
|
||||||
|
tts.set_pitch(tts.min_pitch())?;
|
||||||
|
tts.speak("This is low pitch.", false)?;
|
||||||
|
tts.set_pitch(tts.normal_pitch())?;
|
||||||
|
tts.speak("This is normal pitch.", false)?;
|
||||||
|
tts.set_pitch(original_pitch)?;
|
||||||
|
}
|
||||||
|
let Features { volume, .. } = tts.supported_features();
|
||||||
|
if volume {
|
||||||
|
let original_volume = tts.get_volume()?;
|
||||||
|
tts.set_volume(tts.max_volume())?;
|
||||||
|
tts.speak("This is loud!", false)?;
|
||||||
|
tts.set_volume(tts.min_volume())?;
|
||||||
|
tts.speak("This is quiet.", false)?;
|
||||||
|
tts.set_volume(tts.normal_volume())?;
|
||||||
|
tts.speak("This is normal volume.", false)?;
|
||||||
|
tts.set_volume(original_volume)?;
|
||||||
|
}
|
||||||
|
tts.speak("Goodbye.", false)?;
|
||||||
|
loop {}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))]
|
||||||
|
pub fn main() {
|
||||||
|
run().expect("Failed to run");
|
||||||
|
}
|
89
examples/clone_drop.rs
Normal file
89
examples/clone_drop.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use cocoa_foundation::base::id;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use cocoa_foundation::foundation::NSRunLoop;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use objc::{msg_send, sel, sel_impl};
|
||||||
|
|
||||||
|
use tts::*;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Error> {
|
||||||
|
env_logger::init();
|
||||||
|
let tts = Tts::default()?;
|
||||||
|
if Tts::screen_reader_available() {
|
||||||
|
println!("A screen reader is available on this platform.");
|
||||||
|
} else {
|
||||||
|
println!("No screen reader is available on this platform.");
|
||||||
|
}
|
||||||
|
let Features {
|
||||||
|
utterance_callbacks,
|
||||||
|
..
|
||||||
|
} = tts.supported_features();
|
||||||
|
if utterance_callbacks {
|
||||||
|
tts.on_utterance_begin(Some(Box::new(|utterance| {
|
||||||
|
println!("Started speaking {:?}", utterance)
|
||||||
|
})))?;
|
||||||
|
tts.on_utterance_end(Some(Box::new(|utterance| {
|
||||||
|
println!("Finished speaking {:?}", utterance)
|
||||||
|
})))?;
|
||||||
|
tts.on_utterance_stop(Some(Box::new(|utterance| {
|
||||||
|
println!("Stopped speaking {:?}", utterance)
|
||||||
|
})))?;
|
||||||
|
}
|
||||||
|
let mut tts_clone = tts.clone();
|
||||||
|
drop(tts);
|
||||||
|
let Features { is_speaking, .. } = tts_clone.supported_features();
|
||||||
|
if is_speaking {
|
||||||
|
println!("Are we speaking? {}", tts_clone.is_speaking()?);
|
||||||
|
}
|
||||||
|
tts_clone.speak("Hello, world.", false)?;
|
||||||
|
let Features { rate, .. } = tts_clone.supported_features();
|
||||||
|
if rate {
|
||||||
|
let original_rate = tts_clone.get_rate()?;
|
||||||
|
tts_clone.speak(format!("Current rate: {}", original_rate), false)?;
|
||||||
|
tts_clone.set_rate(tts_clone.max_rate())?;
|
||||||
|
tts_clone.speak("This is very fast.", false)?;
|
||||||
|
tts_clone.set_rate(tts_clone.min_rate())?;
|
||||||
|
tts_clone.speak("This is very slow.", false)?;
|
||||||
|
tts_clone.set_rate(tts_clone.normal_rate())?;
|
||||||
|
tts_clone.speak("This is the normal rate.", false)?;
|
||||||
|
tts_clone.set_rate(original_rate)?;
|
||||||
|
}
|
||||||
|
let Features { pitch, .. } = tts_clone.supported_features();
|
||||||
|
if pitch {
|
||||||
|
let original_pitch = tts_clone.get_pitch()?;
|
||||||
|
tts_clone.set_pitch(tts_clone.max_pitch())?;
|
||||||
|
tts_clone.speak("This is high-pitch.", false)?;
|
||||||
|
tts_clone.set_pitch(tts_clone.min_pitch())?;
|
||||||
|
tts_clone.speak("This is low pitch.", false)?;
|
||||||
|
tts_clone.set_pitch(tts_clone.normal_pitch())?;
|
||||||
|
tts_clone.speak("This is normal pitch.", false)?;
|
||||||
|
tts_clone.set_pitch(original_pitch)?;
|
||||||
|
}
|
||||||
|
let Features { volume, .. } = tts_clone.supported_features();
|
||||||
|
if volume {
|
||||||
|
let original_volume = tts_clone.get_volume()?;
|
||||||
|
tts_clone.set_volume(tts_clone.max_volume())?;
|
||||||
|
tts_clone.speak("This is loud!", false)?;
|
||||||
|
tts_clone.set_volume(tts_clone.min_volume())?;
|
||||||
|
tts_clone.speak("This is quiet.", false)?;
|
||||||
|
tts_clone.set_volume(tts_clone.normal_volume())?;
|
||||||
|
tts_clone.speak("This is normal volume.", false)?;
|
||||||
|
tts_clone.set_volume(original_volume)?;
|
||||||
|
}
|
||||||
|
tts_clone.speak("Goodbye.", false)?;
|
||||||
|
let mut _input = String::new();
|
||||||
|
// The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context.
|
||||||
|
// It shouldn't be needed in an app or game that almost certainly has one already.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let run_loop: id = unsafe { NSRunLoop::currentRunLoop() };
|
||||||
|
unsafe {
|
||||||
|
let _: () = msg_send![run_loop, run];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
io::stdin().read_line(&mut _input)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -11,7 +11,31 @@ use tts::*;
|
||||||
|
|
||||||
fn main() -> Result<(), Error> {
|
fn main() -> Result<(), Error> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let mut tts = TTS::default()?;
|
let mut tts = Tts::default()?;
|
||||||
|
if Tts::screen_reader_available() {
|
||||||
|
println!("A screen reader is available on this platform.");
|
||||||
|
} else {
|
||||||
|
println!("No screen reader is available on this platform.");
|
||||||
|
}
|
||||||
|
let Features {
|
||||||
|
utterance_callbacks,
|
||||||
|
..
|
||||||
|
} = tts.supported_features();
|
||||||
|
if utterance_callbacks {
|
||||||
|
tts.on_utterance_begin(Some(Box::new(|utterance| {
|
||||||
|
println!("Started speaking {:?}", utterance)
|
||||||
|
})))?;
|
||||||
|
tts.on_utterance_end(Some(Box::new(|utterance| {
|
||||||
|
println!("Finished speaking {:?}", utterance)
|
||||||
|
})))?;
|
||||||
|
tts.on_utterance_stop(Some(Box::new(|utterance| {
|
||||||
|
println!("Stopped speaking {:?}", utterance)
|
||||||
|
})))?;
|
||||||
|
}
|
||||||
|
let Features { is_speaking, .. } = tts.supported_features();
|
||||||
|
if is_speaking {
|
||||||
|
println!("Are we speaking? {}", tts.is_speaking()?);
|
||||||
|
}
|
||||||
tts.speak("Hello, world.", false)?;
|
tts.speak("Hello, world.", false)?;
|
||||||
let Features { rate, .. } = tts.supported_features();
|
let Features { rate, .. } = tts.supported_features();
|
||||||
if rate {
|
if rate {
|
||||||
|
@ -47,8 +71,27 @@ fn main() -> Result<(), Error> {
|
||||||
tts.speak("This is normal volume.", false)?;
|
tts.speak("This is normal volume.", false)?;
|
||||||
tts.set_volume(original_volume)?;
|
tts.set_volume(original_volume)?;
|
||||||
}
|
}
|
||||||
|
let Features { voice, .. } = tts.supported_features();
|
||||||
|
if voice {
|
||||||
|
let voices = tts.voices()?;
|
||||||
|
println!("Available voices:\n===");
|
||||||
|
for v in &voices {
|
||||||
|
println!("{:?}", v);
|
||||||
|
}
|
||||||
|
let Features { get_voice, .. } = tts.supported_features();
|
||||||
|
let original_voice = if get_voice { tts.voice()? } else { None };
|
||||||
|
for v in &voices {
|
||||||
|
tts.set_voice(v)?;
|
||||||
|
tts.speak(format!("This is {}.", v.name()), false)?;
|
||||||
|
}
|
||||||
|
if let Some(original_voice) = original_voice {
|
||||||
|
tts.set_voice(&original_voice)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
tts.speak("Goodbye.", false)?;
|
tts.speak("Goodbye.", false)?;
|
||||||
let mut _input = String::new();
|
let mut _input = String::new();
|
||||||
|
// The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context.
|
||||||
|
// It shouldn't be needed in an app or game that almost certainly has one already.
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
let run_loop: id = unsafe { NSRunLoop::currentRunLoop() };
|
let run_loop: id = unsafe { NSRunLoop::currentRunLoop() };
|
||||||
|
|
14
examples/latency.rs
Normal file
14
examples/latency.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use tts::*;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Error> {
|
||||||
|
env_logger::init();
|
||||||
|
let mut tts = Tts::default()?;
|
||||||
|
println!("Press Enter and wait for speech.");
|
||||||
|
loop {
|
||||||
|
let mut _input = String::new();
|
||||||
|
io::stdin().read_line(&mut _input)?;
|
||||||
|
tts.speak("Hello, world.", true)?;
|
||||||
|
}
|
||||||
|
}
|
32
examples/ramble.rs
Normal file
32
examples/ramble.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use cocoa_foundation::base::id;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use cocoa_foundation::foundation::NSDefaultRunLoopMode;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use cocoa_foundation::foundation::NSRunLoop;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use objc::class;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use objc::{msg_send, sel, sel_impl};
|
||||||
|
use std::{thread, time};
|
||||||
|
use tts::*;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Error> {
|
||||||
|
env_logger::init();
|
||||||
|
let mut tts = Tts::default()?;
|
||||||
|
let mut phrase = 1;
|
||||||
|
loop {
|
||||||
|
tts.speak(format!("Phrase {}", phrase), false)?;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let run_loop: id = unsafe { NSRunLoop::currentRunLoop() };
|
||||||
|
unsafe {
|
||||||
|
let date: id = msg_send![class!(NSDate), distantFuture];
|
||||||
|
let _: () = msg_send![run_loop, runMode:NSDefaultRunLoopMode beforeDate:date];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let time = time::Duration::from_secs(5);
|
||||||
|
thread::sleep(time);
|
||||||
|
phrase += 1;
|
||||||
|
}
|
||||||
|
}
|
2
examples/web/.cargo/config
Normal file
2
examples/web/.cargo/config
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[build]
|
||||||
|
target = "wasm32-unknown-unknown"
|
1
examples/web/.gitignore
vendored
Normal file
1
examples/web/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
dist
|
13
examples/web/Cargo.toml
Normal file
13
examples/web/Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "web"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
console_log = "0.2"
|
||||||
|
log = "0.4"
|
||||||
|
seed = "0.9"
|
||||||
|
tts = { path = "../.." }
|
12
examples/web/index.html
Normal file
12
examples/web/index.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Example</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
157
examples/web/src/main.rs
Normal file
157
examples/web/src/main.rs
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
#![allow(clippy::wildcard_imports)]
|
||||||
|
use seed::{prelude::*, *};
|
||||||
|
|
||||||
|
use tts::Tts;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Model {
|
||||||
|
text: String,
|
||||||
|
tts: Tts,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum Msg {
|
||||||
|
TextChanged(String),
|
||||||
|
RateChanged(String),
|
||||||
|
PitchChanged(String),
|
||||||
|
VolumeChanged(String),
|
||||||
|
VoiceChanged(String),
|
||||||
|
Speak,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
|
||||||
|
let mut tts = Tts::default().unwrap();
|
||||||
|
if tts.voices().unwrap().iter().len() > 0 {
|
||||||
|
if tts.voice().unwrap().is_none() {
|
||||||
|
tts.set_voice(tts.voices().unwrap().first().unwrap())
|
||||||
|
.expect("Failed to set voice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Model {
|
||||||
|
text: "Hello, world. This is a test of the current text-to-speech values.".into(),
|
||||||
|
tts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
|
||||||
|
use Msg::*;
|
||||||
|
match msg {
|
||||||
|
TextChanged(text) => model.text = text,
|
||||||
|
RateChanged(rate) => {
|
||||||
|
let rate = rate.parse::<f32>().unwrap();
|
||||||
|
model.tts.set_rate(rate).unwrap();
|
||||||
|
}
|
||||||
|
PitchChanged(pitch) => {
|
||||||
|
let pitch = pitch.parse::<f32>().unwrap();
|
||||||
|
model.tts.set_pitch(pitch).unwrap();
|
||||||
|
}
|
||||||
|
VolumeChanged(volume) => {
|
||||||
|
let volume = volume.parse::<f32>().unwrap();
|
||||||
|
model.tts.set_volume(volume).unwrap();
|
||||||
|
}
|
||||||
|
VoiceChanged(voice) => {
|
||||||
|
for v in model.tts.voices().unwrap() {
|
||||||
|
if v.id() == voice {
|
||||||
|
model.tts.set_voice(&v).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Speak => {
|
||||||
|
model.tts.speak(&model.text, false).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(model: &Model) -> Node<Msg> {
|
||||||
|
let should_show_voices = model.tts.voices().unwrap().iter().len() > 0;
|
||||||
|
form![
|
||||||
|
div![label![
|
||||||
|
"Text to speak",
|
||||||
|
input![
|
||||||
|
attrs! {
|
||||||
|
At::Value => model.text,
|
||||||
|
At::AutoFocus => AtValue::None,
|
||||||
|
},
|
||||||
|
input_ev(Ev::Input, Msg::TextChanged)
|
||||||
|
],
|
||||||
|
],],
|
||||||
|
div![label![
|
||||||
|
"Rate",
|
||||||
|
input![
|
||||||
|
attrs! {
|
||||||
|
At::Type => "number",
|
||||||
|
At::Value => model.tts.get_rate().unwrap(),
|
||||||
|
At::Min => model.tts.min_rate(),
|
||||||
|
At::Max => model.tts.max_rate()
|
||||||
|
},
|
||||||
|
input_ev(Ev::Input, Msg::RateChanged)
|
||||||
|
],
|
||||||
|
],],
|
||||||
|
div![label![
|
||||||
|
"Pitch",
|
||||||
|
input![
|
||||||
|
attrs! {
|
||||||
|
At::Type => "number",
|
||||||
|
At::Value => model.tts.get_pitch().unwrap(),
|
||||||
|
At::Min => model.tts.min_pitch(),
|
||||||
|
At::Max => model.tts.max_pitch()
|
||||||
|
},
|
||||||
|
input_ev(Ev::Input, Msg::PitchChanged)
|
||||||
|
],
|
||||||
|
],],
|
||||||
|
div![label![
|
||||||
|
"Volume",
|
||||||
|
input![
|
||||||
|
attrs! {
|
||||||
|
At::Type => "number",
|
||||||
|
At::Value => model.tts.get_volume().unwrap(),
|
||||||
|
At::Min => model.tts.min_volume(),
|
||||||
|
At::Max => model.tts.max_volume()
|
||||||
|
},
|
||||||
|
input_ev(Ev::Input, Msg::VolumeChanged)
|
||||||
|
],
|
||||||
|
],],
|
||||||
|
if should_show_voices {
|
||||||
|
div![
|
||||||
|
label!["Voice"],
|
||||||
|
select![
|
||||||
|
model.tts.voices().unwrap().iter().map(|v| {
|
||||||
|
let selected = if let Some(voice) = model.tts.voice().unwrap() {
|
||||||
|
voice.id() == v.id()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
option![
|
||||||
|
attrs! {
|
||||||
|
At::Value => v.id()
|
||||||
|
},
|
||||||
|
if selected {
|
||||||
|
attrs! {
|
||||||
|
At::Selected => selected
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attrs! {}
|
||||||
|
},
|
||||||
|
v.name()
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
input_ev(Ev::Change, Msg::VoiceChanged)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
div!["Your browser does not seem to support selecting voices."]
|
||||||
|
},
|
||||||
|
button![
|
||||||
|
"Speak",
|
||||||
|
ev(Ev::Click, |e| {
|
||||||
|
e.prevent_default();
|
||||||
|
Msg::Speak
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
console_log::init().expect("Error initializing logger");
|
||||||
|
App::start("app", init, update, view);
|
||||||
|
}
|
402
src/backends/android.rs
Normal file
402
src/backends/android.rs
Normal file
|
@ -0,0 +1,402 @@
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
ffi::{CStr, CString},
|
||||||
|
os::raw::c_void,
|
||||||
|
sync::{Mutex, RwLock},
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use jni::{
|
||||||
|
objects::{GlobalRef, JObject, JString},
|
||||||
|
sys::{jfloat, jint, JNI_VERSION_1_6},
|
||||||
|
JNIEnv, JavaVM,
|
||||||
|
};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use log::{error, info};
|
||||||
|
|
||||||
|
use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref BRIDGE: Mutex<Option<GlobalRef>> = Mutex::new(None);
|
||||||
|
static ref NEXT_BACKEND_ID: Mutex<u64> = Mutex::new(0);
|
||||||
|
static ref PENDING_INITIALIZATIONS: RwLock<HashSet<u64>> = RwLock::new(HashSet::new());
|
||||||
|
static ref NEXT_UTTERANCE_ID: Mutex<u64> = Mutex::new(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn JNI_OnLoad(vm: JavaVM, _: *mut c_void) -> jint {
|
||||||
|
let mut env = vm.get_env().expect("Cannot get reference to the JNIEnv");
|
||||||
|
let b = env
|
||||||
|
.find_class("rs/tts/Bridge")
|
||||||
|
.expect("Failed to find `Bridge`");
|
||||||
|
let b = env
|
||||||
|
.new_global_ref(b)
|
||||||
|
.expect("Failed to create `Bridge` `GlobalRef`");
|
||||||
|
let mut bridge = BRIDGE.lock().unwrap();
|
||||||
|
*bridge = Some(b);
|
||||||
|
JNI_VERSION_1_6
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(mut env: JNIEnv, obj: JObject, status: jint) {
|
||||||
|
let id = env
|
||||||
|
.get_field(obj, "backendId", "I")
|
||||||
|
.expect("Failed to get backend ID")
|
||||||
|
.i()
|
||||||
|
.expect("Failed to cast to int") as u64;
|
||||||
|
let mut pending = PENDING_INITIALIZATIONS.write().unwrap();
|
||||||
|
(*pending).remove(&id);
|
||||||
|
if status != 0 {
|
||||||
|
error!("Failed to initialize TTS engine");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub unsafe extern "C" fn Java_rs_tts_Bridge_onStart(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
obj: JObject,
|
||||||
|
utterance_id: JString,
|
||||||
|
) {
|
||||||
|
let backend_id = env
|
||||||
|
.get_field(obj, "backendId", "I")
|
||||||
|
.expect("Failed to get backend ID")
|
||||||
|
.i()
|
||||||
|
.expect("Failed to cast to int") as u64;
|
||||||
|
let backend_id = BackendId::Android(backend_id);
|
||||||
|
let utterance_id = CString::from(CStr::from_ptr(
|
||||||
|
env.get_string(&utterance_id).unwrap().as_ptr(),
|
||||||
|
))
|
||||||
|
.into_string()
|
||||||
|
.unwrap();
|
||||||
|
let utterance_id = utterance_id.parse::<u64>().unwrap();
|
||||||
|
let utterance_id = UtteranceId::Android(utterance_id);
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let cb = callbacks.get_mut(&backend_id).unwrap();
|
||||||
|
if let Some(f) = cb.utterance_begin.as_mut() {
|
||||||
|
f(utterance_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub unsafe extern "C" fn Java_rs_tts_Bridge_onStop(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
obj: JObject,
|
||||||
|
utterance_id: JString,
|
||||||
|
) {
|
||||||
|
let backend_id = env
|
||||||
|
.get_field(obj, "backendId", "I")
|
||||||
|
.expect("Failed to get backend ID")
|
||||||
|
.i()
|
||||||
|
.expect("Failed to cast to int") as u64;
|
||||||
|
let backend_id = BackendId::Android(backend_id);
|
||||||
|
let utterance_id = CString::from(CStr::from_ptr(
|
||||||
|
env.get_string(&utterance_id).unwrap().as_ptr(),
|
||||||
|
))
|
||||||
|
.into_string()
|
||||||
|
.unwrap();
|
||||||
|
let utterance_id = utterance_id.parse::<u64>().unwrap();
|
||||||
|
let utterance_id = UtteranceId::Android(utterance_id);
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let cb = callbacks.get_mut(&backend_id).unwrap();
|
||||||
|
if let Some(f) = cb.utterance_end.as_mut() {
|
||||||
|
f(utterance_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub unsafe extern "C" fn Java_rs_tts_Bridge_onDone(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
obj: JObject,
|
||||||
|
utterance_id: JString,
|
||||||
|
) {
|
||||||
|
let backend_id = env
|
||||||
|
.get_field(obj, "backendId", "I")
|
||||||
|
.expect("Failed to get backend ID")
|
||||||
|
.i()
|
||||||
|
.expect("Failed to cast to int") as u64;
|
||||||
|
let backend_id = BackendId::Android(backend_id);
|
||||||
|
let utterance_id = CString::from(CStr::from_ptr(
|
||||||
|
env.get_string(&utterance_id).unwrap().as_ptr(),
|
||||||
|
))
|
||||||
|
.into_string()
|
||||||
|
.unwrap();
|
||||||
|
let utterance_id = utterance_id.parse::<u64>().unwrap();
|
||||||
|
let utterance_id = UtteranceId::Android(utterance_id);
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let cb = callbacks.get_mut(&backend_id).unwrap();
|
||||||
|
if let Some(f) = cb.utterance_stop.as_mut() {
|
||||||
|
f(utterance_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub unsafe extern "C" fn Java_rs_tts_Bridge_onError(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
obj: JObject,
|
||||||
|
utterance_id: JString,
|
||||||
|
) {
|
||||||
|
let backend_id = env
|
||||||
|
.get_field(obj, "backendId", "I")
|
||||||
|
.expect("Failed to get backend ID")
|
||||||
|
.i()
|
||||||
|
.expect("Failed to cast to int") as u64;
|
||||||
|
let backend_id = BackendId::Android(backend_id);
|
||||||
|
let utterance_id = CString::from(CStr::from_ptr(
|
||||||
|
env.get_string(&utterance_id).unwrap().as_ptr(),
|
||||||
|
))
|
||||||
|
.into_string()
|
||||||
|
.unwrap();
|
||||||
|
let utterance_id = utterance_id.parse::<u64>().unwrap();
|
||||||
|
let utterance_id = UtteranceId::Android(utterance_id);
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let cb = callbacks.get_mut(&backend_id).unwrap();
|
||||||
|
if let Some(f) = cb.utterance_end.as_mut() {
|
||||||
|
f(utterance_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct Android {
|
||||||
|
id: BackendId,
|
||||||
|
tts: GlobalRef,
|
||||||
|
rate: f32,
|
||||||
|
pitch: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Android {
|
||||||
|
pub(crate) fn new() -> Result<Self, Error> {
|
||||||
|
info!("Initializing Android backend");
|
||||||
|
let mut backend_id = NEXT_BACKEND_ID.lock().unwrap();
|
||||||
|
let bid = *backend_id;
|
||||||
|
let id = BackendId::Android(bid);
|
||||||
|
*backend_id += 1;
|
||||||
|
drop(backend_id);
|
||||||
|
let ctx = ndk_context::android_context();
|
||||||
|
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }?;
|
||||||
|
let context = unsafe { JObject::from_raw(ctx.context().cast()) };
|
||||||
|
let mut env = vm.attach_current_thread_permanently()?;
|
||||||
|
let bridge = BRIDGE.lock().unwrap();
|
||||||
|
if let Some(bridge) = &*bridge {
|
||||||
|
let bridge = env.new_object(bridge, "(I)V", &[(bid as jint).into()])?;
|
||||||
|
let tts = env.new_object(
|
||||||
|
"android/speech/tts/TextToSpeech",
|
||||||
|
"(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V",
|
||||||
|
&[(&context).into(), (&bridge).into()],
|
||||||
|
)?;
|
||||||
|
env.call_method(
|
||||||
|
&tts,
|
||||||
|
"setOnUtteranceProgressListener",
|
||||||
|
"(Landroid/speech/tts/UtteranceProgressListener;)I",
|
||||||
|
&[(&bridge).into()],
|
||||||
|
)?;
|
||||||
|
{
|
||||||
|
let mut pending = PENDING_INITIALIZATIONS.write().unwrap();
|
||||||
|
(*pending).insert(bid);
|
||||||
|
}
|
||||||
|
let tts = env.new_global_ref(tts)?;
|
||||||
|
// This hack makes my brain bleed.
|
||||||
|
const MAX_WAIT_TIME: Duration = Duration::from_millis(500);
|
||||||
|
let start = Instant::now();
|
||||||
|
// Wait a max of 500ms for initialization, then return an error to avoid hanging.
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
let pending = PENDING_INITIALIZATIONS.read().unwrap();
|
||||||
|
if !(*pending).contains(&bid) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if start.elapsed() > MAX_WAIT_TIME {
|
||||||
|
return Err(Error::OperationFailed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(5));
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
id,
|
||||||
|
tts,
|
||||||
|
rate: 1.,
|
||||||
|
pitch: 1.,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(Error::NoneError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vm() -> Result<JavaVM, jni::errors::Error> {
|
||||||
|
let ctx = ndk_context::android_context();
|
||||||
|
unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backend for Android {
|
||||||
|
fn id(&self) -> Option<BackendId> {
|
||||||
|
Some(self.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supported_features(&self) -> Features {
|
||||||
|
Features {
|
||||||
|
stop: true,
|
||||||
|
rate: true,
|
||||||
|
pitch: true,
|
||||||
|
volume: false,
|
||||||
|
is_speaking: true,
|
||||||
|
utterance_callbacks: true,
|
||||||
|
voice: false,
|
||||||
|
get_voice: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error> {
|
||||||
|
let vm = Self::vm()?;
|
||||||
|
let mut env = vm.get_env()?;
|
||||||
|
let tts = self.tts.as_obj();
|
||||||
|
let text = env.new_string(text)?;
|
||||||
|
let queue_mode = if interrupt { 0 } else { 1 };
|
||||||
|
let mut utterance_id = NEXT_UTTERANCE_ID.lock().unwrap();
|
||||||
|
let uid = *utterance_id;
|
||||||
|
*utterance_id += 1;
|
||||||
|
drop(utterance_id);
|
||||||
|
let id = UtteranceId::Android(uid);
|
||||||
|
let uid = env.new_string(uid.to_string())?;
|
||||||
|
let rv = env.call_method(
|
||||||
|
tts,
|
||||||
|
"speak",
|
||||||
|
"(Ljava/lang/CharSequence;ILandroid/os/Bundle;Ljava/lang/String;)I",
|
||||||
|
&[
|
||||||
|
(&text).into(),
|
||||||
|
queue_mode.into(),
|
||||||
|
(&JObject::null()).into(),
|
||||||
|
(&uid).into(),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
let rv = rv.i()?;
|
||||||
|
if rv == 0 {
|
||||||
|
Ok(Some(id))
|
||||||
|
} else {
|
||||||
|
Err(Error::OperationFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&mut self) -> Result<(), Error> {
|
||||||
|
let vm = Self::vm()?;
|
||||||
|
let mut env = vm.get_env()?;
|
||||||
|
let tts = self.tts.as_obj();
|
||||||
|
let rv = env.call_method(tts, "stop", "()I", &[])?;
|
||||||
|
let rv = rv.i()?;
|
||||||
|
if rv == 0 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::OperationFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn min_rate(&self) -> f32 {
|
||||||
|
0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_rate(&self) -> f32 {
|
||||||
|
10.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normal_rate(&self) -> f32 {
|
||||||
|
1.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_rate(&self) -> Result<f32, Error> {
|
||||||
|
Ok(self.rate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_rate(&mut self, rate: f32) -> Result<(), Error> {
|
||||||
|
let vm = Self::vm()?;
|
||||||
|
let mut env = vm.get_env()?;
|
||||||
|
let tts = self.tts.as_obj();
|
||||||
|
let rate = rate as jfloat;
|
||||||
|
let rv = env.call_method(tts, "setSpeechRate", "(F)I", &[rate.into()])?;
|
||||||
|
let rv = rv.i()?;
|
||||||
|
if rv == 0 {
|
||||||
|
self.rate = rate;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::OperationFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn min_pitch(&self) -> f32 {
|
||||||
|
0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_pitch(&self) -> f32 {
|
||||||
|
2.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normal_pitch(&self) -> f32 {
|
||||||
|
1.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_pitch(&self) -> Result<f32, Error> {
|
||||||
|
Ok(self.pitch)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> {
|
||||||
|
let vm = Self::vm()?;
|
||||||
|
let mut env = vm.get_env()?;
|
||||||
|
let tts = self.tts.as_obj();
|
||||||
|
let pitch = pitch as jfloat;
|
||||||
|
let rv = env.call_method(tts, "setPitch", "(F)I", &[pitch.into()])?;
|
||||||
|
let rv = rv.i()?;
|
||||||
|
if rv == 0 {
|
||||||
|
self.pitch = pitch;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::OperationFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn min_volume(&self) -> f32 {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_volume(&self) -> f32 {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normal_volume(&self) -> f32 {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_volume(&self) -> Result<f32, Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_volume(&mut self, _volume: f32) -> Result<(), Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_speaking(&self) -> Result<bool, Error> {
|
||||||
|
let vm = Self::vm()?;
|
||||||
|
let mut env = vm.get_env()?;
|
||||||
|
let tts = self.tts.as_obj();
|
||||||
|
let rv = env.call_method(tts, "isSpeaking", "()Z", &[])?;
|
||||||
|
let rv = rv.z()?;
|
||||||
|
Ok(rv)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn voice(&self) -> Result<Option<Voice>, Error> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn voices(&self) -> Result<Vec<Voice>, Error> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_voice(&mut self, _voice: &Voice) -> Result<(), Error> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
#[link(name = "AppKit", kind = "framework")]
|
|
||||||
use cocoa_foundation::base::{id, nil};
|
use cocoa_foundation::base::{id, nil};
|
||||||
use cocoa_foundation::foundation::NSString;
|
use cocoa_foundation::foundation::NSString;
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
|
@ -7,17 +6,18 @@ use objc::declare::ClassDecl;
|
||||||
use objc::runtime::*;
|
use objc::runtime::*;
|
||||||
use objc::*;
|
use objc::*;
|
||||||
|
|
||||||
use crate::{Backend, Error, Features};
|
use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice};
|
||||||
|
|
||||||
pub struct AppKit(*mut Object, *mut Object);
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct AppKit(*mut Object, *mut Object);
|
||||||
|
|
||||||
impl AppKit {
|
impl AppKit {
|
||||||
pub fn new() -> Self {
|
pub(crate) fn new() -> Result<Self, Error> {
|
||||||
info!("Initializing AppKit backend");
|
info!("Initializing AppKit backend");
|
||||||
unsafe {
|
unsafe {
|
||||||
let obj: *mut Object = msg_send![class!(NSSpeechSynthesizer), new];
|
let obj: *mut Object = msg_send![class!(NSSpeechSynthesizer), new];
|
||||||
let mut decl =
|
let mut decl = ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject))
|
||||||
ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject)).unwrap();
|
.ok_or(Error::OperationFailed)?;
|
||||||
decl.add_ivar::<id>("synth");
|
decl.add_ivar::<id>("synth");
|
||||||
decl.add_ivar::<id>("strings");
|
decl.add_ivar::<id>("strings");
|
||||||
|
|
||||||
|
@ -46,16 +46,18 @@ impl AppKit {
|
||||||
) {
|
) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let strings: id = *this.get_ivar("strings");
|
let strings: id = *this.get_ivar("strings");
|
||||||
let str: id = msg_send!(strings, firstObject);
|
|
||||||
let _: () = msg_send![str, release];
|
|
||||||
let _: () = msg_send!(strings, removeObjectAtIndex:0);
|
|
||||||
let count: u32 = msg_send![strings, count];
|
let count: u32 = msg_send![strings, count];
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
|
let str: id = msg_send!(strings, firstObject);
|
||||||
|
let _: () = msg_send![str, release];
|
||||||
|
let _: () = msg_send!(strings, removeObjectAtIndex:0);
|
||||||
|
if count > 1 {
|
||||||
let str: id = msg_send!(strings, firstObject);
|
let str: id = msg_send!(strings, firstObject);
|
||||||
let _: BOOL = msg_send![synth, startSpeakingString: str];
|
let _: BOOL = msg_send![synth, startSpeakingString: str];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
decl.add_method(
|
decl.add_method(
|
||||||
sel!(speechSynthesizer:didFinishSpeaking:),
|
sel!(speechSynthesizer:didFinishSpeaking:),
|
||||||
speech_synthesizer_did_finish_speaking
|
speech_synthesizer_did_finish_speaking
|
||||||
|
@ -81,27 +83,37 @@ impl AppKit {
|
||||||
|
|
||||||
let delegate_class = decl.register();
|
let delegate_class = decl.register();
|
||||||
let delegate_obj: *mut Object = msg_send![delegate_class, new];
|
let delegate_obj: *mut Object = msg_send![delegate_class, new];
|
||||||
delegate_obj.as_mut().unwrap().set_ivar("synth", obj);
|
delegate_obj
|
||||||
|
.as_mut()
|
||||||
|
.ok_or(Error::OperationFailed)?
|
||||||
|
.set_ivar("synth", obj);
|
||||||
let strings: id = msg_send![class!(NSMutableArray), new];
|
let strings: id = msg_send![class!(NSMutableArray), new];
|
||||||
delegate_obj.as_mut().unwrap().set_ivar("strings", strings);
|
delegate_obj
|
||||||
|
.as_mut()
|
||||||
|
.ok_or(Error::OperationFailed)?
|
||||||
|
.set_ivar("strings", strings);
|
||||||
let _: Object = msg_send![obj, setDelegate: delegate_obj];
|
let _: Object = msg_send![obj, setDelegate: delegate_obj];
|
||||||
AppKit(obj, delegate_obj)
|
Ok(AppKit(obj, delegate_obj))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend for AppKit {
|
impl Backend for AppKit {
|
||||||
|
fn id(&self) -> Option<BackendId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn supported_features(&self) -> Features {
|
fn supported_features(&self) -> Features {
|
||||||
Features {
|
Features {
|
||||||
stop: true,
|
stop: true,
|
||||||
rate: true,
|
rate: true,
|
||||||
pitch: false,
|
|
||||||
volume: true,
|
volume: true,
|
||||||
is_speaking: true,
|
is_speaking: true,
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error> {
|
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error> {
|
||||||
trace!("speak({}, {})", text, interrupt);
|
trace!("speak({}, {})", text, interrupt);
|
||||||
if interrupt {
|
if interrupt {
|
||||||
self.stop()?;
|
self.stop()?;
|
||||||
|
@ -110,7 +122,7 @@ impl Backend for AppKit {
|
||||||
let str = NSString::alloc(nil).init_str(text);
|
let str = NSString::alloc(nil).init_str(text);
|
||||||
let _: () = msg_send![self.1, enqueueAndSpeak: str];
|
let _: () = msg_send![self.1, enqueueAndSpeak: str];
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> Result<(), Error> {
|
fn stop(&mut self) -> Result<(), Error> {
|
||||||
|
@ -193,7 +205,19 @@ impl Backend for AppKit {
|
||||||
|
|
||||||
fn is_speaking(&self) -> Result<bool, Error> {
|
fn is_speaking(&self) -> Result<bool, Error> {
|
||||||
let is_speaking: i8 = unsafe { msg_send![self.0, isSpeaking] };
|
let is_speaking: i8 = unsafe { msg_send![self.0, isSpeaking] };
|
||||||
Ok(is_speaking == YES)
|
Ok(is_speaking != NO as i8)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn voice(&self) -> Result<Option<Voice>, Error> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn voices(&self) -> Result<Vec<Voice>, Error> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_voice(&mut self, _voice: &Voice) -> Result<(), Error> {
|
||||||
|
unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,165 @@
|
||||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
#[link(name = "AVFoundation", kind = "framework")]
|
use std::sync::Mutex;
|
||||||
use cocoa_foundation::base::{id, nil};
|
|
||||||
|
use cocoa_foundation::base::{id, nil, NO};
|
||||||
use cocoa_foundation::foundation::NSString;
|
use cocoa_foundation::foundation::NSString;
|
||||||
|
use core_foundation::array::CFArray;
|
||||||
|
use core_foundation::base::TCFType;
|
||||||
|
use core_foundation::string::CFString;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
use objc::runtime::*;
|
use objc::runtime::{Object, Sel};
|
||||||
use objc::*;
|
use objc::{class, declare::ClassDecl, msg_send, sel, sel_impl};
|
||||||
|
use oxilangtag::LanguageTag;
|
||||||
|
|
||||||
use crate::{Backend, Error, Features};
|
use crate::{Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, CALLBACKS};
|
||||||
|
|
||||||
pub struct AvFoundation {
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct AvFoundation {
|
||||||
|
id: BackendId,
|
||||||
|
delegate: *mut Object,
|
||||||
synth: *mut Object,
|
synth: *mut Object,
|
||||||
rate: f32,
|
rate: f32,
|
||||||
volume: f32,
|
volume: f32,
|
||||||
pitch: f32,
|
pitch: f32,
|
||||||
|
voice: Option<Voice>,
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref NEXT_BACKEND_ID: Mutex<u64> = Mutex::new(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AvFoundation {
|
impl AvFoundation {
|
||||||
pub fn new() -> Self {
|
pub(crate) fn new() -> Result<Self, Error> {
|
||||||
info!("Initializing AVFoundation backend");
|
info!("Initializing AVFoundation backend");
|
||||||
|
let mut decl = ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject))
|
||||||
|
.ok_or(Error::OperationFailed)?;
|
||||||
|
decl.add_ivar::<u64>("backend_id");
|
||||||
|
|
||||||
|
extern "C" fn speech_synthesizer_did_start_speech_utterance(
|
||||||
|
this: &Object,
|
||||||
|
_: Sel,
|
||||||
|
_synth: *const Object,
|
||||||
|
utterance: id,
|
||||||
|
) {
|
||||||
|
trace!("speech_synthesizer_did_start_speech_utterance");
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let backend_id: u64 = *this.get_ivar("backend_id");
|
||||||
|
let backend_id = BackendId::AvFoundation(backend_id);
|
||||||
|
trace!("Locking callbacks");
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
trace!("Locked");
|
||||||
|
let callbacks = callbacks.get_mut(&backend_id).unwrap();
|
||||||
|
if let Some(callback) = callbacks.utterance_begin.as_mut() {
|
||||||
|
trace!("Calling utterance_begin");
|
||||||
|
let utterance_id = UtteranceId::AvFoundation(utterance);
|
||||||
|
callback(utterance_id);
|
||||||
|
trace!("Called");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trace!("Done speech_synthesizer_did_start_speech_utterance");
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" fn speech_synthesizer_did_finish_speech_utterance(
|
||||||
|
this: &Object,
|
||||||
|
_: Sel,
|
||||||
|
_synth: *const Object,
|
||||||
|
utterance: id,
|
||||||
|
) {
|
||||||
|
trace!("speech_synthesizer_did_finish_speech_utterance");
|
||||||
|
unsafe {
|
||||||
|
let backend_id: u64 = *this.get_ivar("backend_id");
|
||||||
|
let backend_id = BackendId::AvFoundation(backend_id);
|
||||||
|
trace!("Locking callbacks");
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
trace!("Locked");
|
||||||
|
let callbacks = callbacks.get_mut(&backend_id).unwrap();
|
||||||
|
if let Some(callback) = callbacks.utterance_end.as_mut() {
|
||||||
|
trace!("Calling utterance_end");
|
||||||
|
let utterance_id = UtteranceId::AvFoundation(utterance);
|
||||||
|
callback(utterance_id);
|
||||||
|
trace!("Called");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trace!("Done speech_synthesizer_did_finish_speech_utterance");
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" fn speech_synthesizer_did_cancel_speech_utterance(
|
||||||
|
this: &Object,
|
||||||
|
_: Sel,
|
||||||
|
_synth: *const Object,
|
||||||
|
utterance: id,
|
||||||
|
) {
|
||||||
|
trace!("speech_synthesizer_did_cancel_speech_utterance");
|
||||||
|
unsafe {
|
||||||
|
let backend_id: u64 = *this.get_ivar("backend_id");
|
||||||
|
let backend_id = BackendId::AvFoundation(backend_id);
|
||||||
|
trace!("Locking callbacks");
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
trace!("Locked");
|
||||||
|
let callbacks = callbacks.get_mut(&backend_id).unwrap();
|
||||||
|
if let Some(callback) = callbacks.utterance_stop.as_mut() {
|
||||||
|
trace!("Calling utterance_stop");
|
||||||
|
let utterance_id = UtteranceId::AvFoundation(utterance);
|
||||||
|
callback(utterance_id);
|
||||||
|
trace!("Called");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trace!("Done speech_synthesizer_did_cancel_speech_utterance");
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
decl.add_method(
|
||||||
|
sel!(speechSynthesizer:didStartSpeechUtterance:),
|
||||||
|
speech_synthesizer_did_start_speech_utterance
|
||||||
|
as extern "C" fn(&Object, Sel, *const Object, id) -> (),
|
||||||
|
);
|
||||||
|
decl.add_method(
|
||||||
|
sel!(speechSynthesizer:didFinishSpeechUtterance:),
|
||||||
|
speech_synthesizer_did_finish_speech_utterance
|
||||||
|
as extern "C" fn(&Object, Sel, *const Object, id) -> (),
|
||||||
|
);
|
||||||
|
decl.add_method(
|
||||||
|
sel!(speechSynthesizer:didCancelSpeechUtterance:),
|
||||||
|
speech_synthesizer_did_cancel_speech_utterance
|
||||||
|
as extern "C" fn(&Object, Sel, *const Object, id) -> (),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let delegate_class = decl.register();
|
||||||
|
let delegate_obj: *mut Object = unsafe { msg_send![delegate_class, new] };
|
||||||
|
let mut backend_id = NEXT_BACKEND_ID.lock().unwrap();
|
||||||
|
let rv = unsafe {
|
||||||
|
trace!("Creating synth");
|
||||||
let synth: *mut Object = msg_send![class!(AVSpeechSynthesizer), new];
|
let synth: *mut Object = msg_send![class!(AVSpeechSynthesizer), new];
|
||||||
|
trace!("Allocated {:?}", synth);
|
||||||
|
delegate_obj
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.set_ivar("backend_id", *backend_id);
|
||||||
|
trace!("Set backend ID in delegate");
|
||||||
|
let _: () = msg_send![synth, setDelegate: delegate_obj];
|
||||||
|
trace!("Assigned delegate: {:?}", delegate_obj);
|
||||||
AvFoundation {
|
AvFoundation {
|
||||||
synth: synth,
|
id: BackendId::AvFoundation(*backend_id),
|
||||||
|
delegate: delegate_obj,
|
||||||
|
synth,
|
||||||
rate: 0.5,
|
rate: 0.5,
|
||||||
volume: 1.,
|
volume: 1.,
|
||||||
pitch: 1.,
|
pitch: 1.,
|
||||||
|
voice: None,
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
*backend_id += 1;
|
||||||
|
Ok(rv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend for AvFoundation {
|
impl Backend for AvFoundation {
|
||||||
|
fn id(&self) -> Option<BackendId> {
|
||||||
|
Some(self.id)
|
||||||
|
}
|
||||||
|
|
||||||
fn supported_features(&self) -> Features {
|
fn supported_features(&self) -> Features {
|
||||||
Features {
|
Features {
|
||||||
stop: true,
|
stop: true,
|
||||||
|
@ -38,24 +167,43 @@ impl Backend for AvFoundation {
|
||||||
pitch: true,
|
pitch: true,
|
||||||
volume: true,
|
volume: true,
|
||||||
is_speaking: true,
|
is_speaking: true,
|
||||||
|
voice: true,
|
||||||
|
get_voice: false,
|
||||||
|
utterance_callbacks: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error> {
|
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error> {
|
||||||
trace!("speak({}, {})", text, interrupt);
|
trace!("speak({}, {})", text, interrupt);
|
||||||
if interrupt {
|
if interrupt && self.is_speaking()? {
|
||||||
self.stop()?;
|
self.stop()?;
|
||||||
}
|
}
|
||||||
|
let mut utterance: id;
|
||||||
unsafe {
|
unsafe {
|
||||||
let str = NSString::alloc(nil).init_str(text);
|
trace!("Allocating utterance string");
|
||||||
let utterance: id = msg_send![class!(AVSpeechUtterance), alloc];
|
let mut str = NSString::alloc(nil);
|
||||||
let _: () = msg_send![utterance, initWithString: str];
|
str = str.init_str(text);
|
||||||
|
trace!("Allocating utterance");
|
||||||
|
utterance = msg_send![class!(AVSpeechUtterance), alloc];
|
||||||
|
trace!("Initializing utterance");
|
||||||
|
utterance = msg_send![utterance, initWithString: str];
|
||||||
|
trace!("Setting rate to {}", self.rate);
|
||||||
let _: () = msg_send![utterance, setRate: self.rate];
|
let _: () = msg_send![utterance, setRate: self.rate];
|
||||||
|
trace!("Setting volume to {}", self.volume);
|
||||||
let _: () = msg_send![utterance, setVolume: self.volume];
|
let _: () = msg_send![utterance, setVolume: self.volume];
|
||||||
|
trace!("Setting pitch to {}", self.pitch);
|
||||||
let _: () = msg_send![utterance, setPitchMultiplier: self.pitch];
|
let _: () = msg_send![utterance, setPitchMultiplier: self.pitch];
|
||||||
let _: () = msg_send![self.synth, speakUtterance: utterance];
|
if let Some(voice) = &self.voice {
|
||||||
|
let mut vid = NSString::alloc(nil);
|
||||||
|
vid = vid.init_str(&voice.id());
|
||||||
|
let v: id = msg_send![class!(AVSpeechSynthesisVoice), voiceWithIdentifier: vid];
|
||||||
|
let _: () = msg_send![utterance, setVoice: v];
|
||||||
}
|
}
|
||||||
Ok(())
|
trace!("Enqueuing");
|
||||||
|
let _: () = msg_send![self.synth, speakUtterance: utterance];
|
||||||
|
trace!("Done queuing");
|
||||||
|
}
|
||||||
|
Ok(Some(UtteranceId::AvFoundation(utterance)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> Result<(), Error> {
|
fn stop(&mut self) -> Result<(), Error> {
|
||||||
|
@ -105,6 +253,7 @@ impl Backend for AvFoundation {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> {
|
fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> {
|
||||||
|
trace!("set_pitch({})", pitch);
|
||||||
self.pitch = pitch;
|
self.pitch = pitch;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -126,19 +275,65 @@ impl Backend for AvFoundation {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_volume(&mut self, volume: f32) -> Result<(), Error> {
|
fn set_volume(&mut self, volume: f32) -> Result<(), Error> {
|
||||||
|
trace!("set_volume({})", volume);
|
||||||
self.volume = volume;
|
self.volume = volume;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_speaking(&self) -> Result<bool, Error> {
|
fn is_speaking(&self) -> Result<bool, Error> {
|
||||||
|
trace!("is_speaking()");
|
||||||
let is_speaking: i8 = unsafe { msg_send![self.synth, isSpeaking] };
|
let is_speaking: i8 = unsafe { msg_send![self.synth, isSpeaking] };
|
||||||
Ok(is_speaking == 1)
|
Ok(is_speaking != NO as i8)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn voice(&self) -> Result<Option<Voice>, Error> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn voices(&self) -> Result<Vec<Voice>, Error> {
|
||||||
|
let voices: CFArray = unsafe {
|
||||||
|
CFArray::wrap_under_get_rule(msg_send![class!(AVSpeechSynthesisVoice), speechVoices])
|
||||||
|
};
|
||||||
|
let rv = voices
|
||||||
|
.iter()
|
||||||
|
.map(|v| {
|
||||||
|
let id: CFString = unsafe {
|
||||||
|
CFString::wrap_under_get_rule(msg_send![*v as *const Object, identifier])
|
||||||
|
};
|
||||||
|
let name: CFString =
|
||||||
|
unsafe { CFString::wrap_under_get_rule(msg_send![*v as *const Object, name]) };
|
||||||
|
let gender: i64 = unsafe { msg_send![*v as *const Object, gender] };
|
||||||
|
let gender = match gender {
|
||||||
|
1 => Some(Gender::Male),
|
||||||
|
2 => Some(Gender::Female),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let language: CFString = unsafe {
|
||||||
|
CFString::wrap_under_get_rule(msg_send![*v as *const Object, language])
|
||||||
|
};
|
||||||
|
let language = language.to_string();
|
||||||
|
let language = LanguageTag::parse(language).unwrap();
|
||||||
|
Voice {
|
||||||
|
id: id.to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
gender,
|
||||||
|
language,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(rv)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> {
|
||||||
|
self.voice = Some(voice.clone());
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for AvFoundation {
|
impl Drop for AvFoundation {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let _: Object = msg_send![self.delegate, release];
|
||||||
let _: Object = msg_send![self.synth, release];
|
let _: Object = msg_send![self.synth, release];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
mod speech_dispatcher;
|
mod speech_dispatcher;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(all(windows, feature = "tolk"))]
|
||||||
mod tolk;
|
mod tolk;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub(crate) mod winrt;
|
mod winrt;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
mod web;
|
mod web;
|
||||||
|
@ -16,17 +16,26 @@ mod appkit;
|
||||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
mod av_foundation;
|
mod av_foundation;
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
mod android;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub use self::speech_dispatcher::*;
|
pub(crate) use self::speech_dispatcher::*;
|
||||||
|
|
||||||
|
#[cfg(all(windows, feature = "tolk"))]
|
||||||
|
pub(crate) use self::tolk::*;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub use self::tolk::*;
|
pub(crate) use self::winrt::*;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub use self::web::*;
|
pub(crate) use self::web::*;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub use self::appkit::*;
|
pub(crate) use self::appkit::*;
|
||||||
|
|
||||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
pub use self::av_foundation::*;
|
pub(crate) use self::av_foundation::*;
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub(crate) use self::android::*;
|
||||||
|
|
|
@ -1,49 +1,116 @@
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
use std::{collections::HashMap, sync::Mutex};
|
||||||
|
|
||||||
|
use lazy_static::*;
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
|
use oxilangtag::LanguageTag;
|
||||||
use speech_dispatcher::*;
|
use speech_dispatcher::*;
|
||||||
|
|
||||||
use crate::{Backend, Error, Features};
|
use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS};
|
||||||
|
|
||||||
pub struct SpeechDispatcher(Connection);
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct SpeechDispatcher(Connection);
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref SPEAKING: Mutex<HashMap<usize, bool>> = {
|
||||||
|
let m: HashMap<usize, bool> = HashMap::new();
|
||||||
|
Mutex::new(m)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
impl SpeechDispatcher {
|
impl SpeechDispatcher {
|
||||||
pub fn new() -> Self {
|
pub(crate) fn new() -> std::result::Result<Self, Error> {
|
||||||
info!("Initializing SpeechDispatcher backend");
|
info!("Initializing SpeechDispatcher backend");
|
||||||
let connection = speech_dispatcher::Connection::open("tts", "tts", "tts", Mode::Single);
|
let connection = speech_dispatcher::Connection::open("tts", "tts", "tts", Mode::Threaded)?;
|
||||||
SpeechDispatcher(connection)
|
let sd = SpeechDispatcher(connection);
|
||||||
|
let mut speaking = SPEAKING.lock().unwrap();
|
||||||
|
speaking.insert(sd.0.client_id(), false);
|
||||||
|
sd.0.on_begin(Some(Box::new(|msg_id, client_id| {
|
||||||
|
let mut speaking = SPEAKING.lock().unwrap();
|
||||||
|
speaking.insert(client_id, true);
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let backend_id = BackendId::SpeechDispatcher(client_id);
|
||||||
|
let cb = callbacks.get_mut(&backend_id).unwrap();
|
||||||
|
let utterance_id = UtteranceId::SpeechDispatcher(msg_id as u64);
|
||||||
|
if let Some(f) = cb.utterance_begin.as_mut() {
|
||||||
|
f(utterance_id);
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
sd.0.on_end(Some(Box::new(|msg_id, client_id| {
|
||||||
|
let mut speaking = SPEAKING.lock().unwrap();
|
||||||
|
speaking.insert(client_id, false);
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let backend_id = BackendId::SpeechDispatcher(client_id);
|
||||||
|
let cb = callbacks.get_mut(&backend_id).unwrap();
|
||||||
|
let utterance_id = UtteranceId::SpeechDispatcher(msg_id as u64);
|
||||||
|
if let Some(f) = cb.utterance_end.as_mut() {
|
||||||
|
f(utterance_id);
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
sd.0.on_cancel(Some(Box::new(|msg_id, client_id| {
|
||||||
|
let mut speaking = SPEAKING.lock().unwrap();
|
||||||
|
speaking.insert(client_id, false);
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let backend_id = BackendId::SpeechDispatcher(client_id);
|
||||||
|
let cb = callbacks.get_mut(&backend_id).unwrap();
|
||||||
|
let utterance_id = UtteranceId::SpeechDispatcher(msg_id as u64);
|
||||||
|
if let Some(f) = cb.utterance_stop.as_mut() {
|
||||||
|
f(utterance_id);
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
sd.0.on_pause(Some(Box::new(|_msg_id, client_id| {
|
||||||
|
let mut speaking = SPEAKING.lock().unwrap();
|
||||||
|
speaking.insert(client_id, false);
|
||||||
|
})));
|
||||||
|
sd.0.on_resume(Some(Box::new(|_msg_id, client_id| {
|
||||||
|
let mut speaking = SPEAKING.lock().unwrap();
|
||||||
|
speaking.insert(client_id, true);
|
||||||
|
})));
|
||||||
|
Ok(sd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend for SpeechDispatcher {
|
impl Backend for SpeechDispatcher {
|
||||||
|
fn id(&self) -> Option<BackendId> {
|
||||||
|
Some(BackendId::SpeechDispatcher(self.0.client_id()))
|
||||||
|
}
|
||||||
|
|
||||||
fn supported_features(&self) -> Features {
|
fn supported_features(&self) -> Features {
|
||||||
Features {
|
Features {
|
||||||
stop: true,
|
stop: true,
|
||||||
rate: true,
|
rate: true,
|
||||||
pitch: true,
|
pitch: true,
|
||||||
volume: true,
|
volume: true,
|
||||||
is_speaking: false,
|
is_speaking: true,
|
||||||
|
voice: true,
|
||||||
|
get_voice: false,
|
||||||
|
utterance_callbacks: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error> {
|
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error> {
|
||||||
trace!("speak({}, {})", text, interrupt);
|
trace!("speak({}, {})", text, interrupt);
|
||||||
if interrupt {
|
if interrupt {
|
||||||
self.stop()?;
|
self.stop()?;
|
||||||
}
|
}
|
||||||
let single_char = text.to_string().capacity() == 1;
|
let single_char = text.to_string().capacity() == 1;
|
||||||
if single_char {
|
if single_char {
|
||||||
self.0.set_punctuation(Punctuation::All);
|
self.0.set_punctuation(Punctuation::All)?;
|
||||||
}
|
}
|
||||||
self.0.say(Priority::Important, text);
|
let id = self.0.say(Priority::Important, text);
|
||||||
if single_char {
|
if single_char {
|
||||||
self.0.set_punctuation(Punctuation::None);
|
self.0.set_punctuation(Punctuation::None)?;
|
||||||
|
}
|
||||||
|
if let Some(id) = id {
|
||||||
|
Ok(Some(UtteranceId::SpeechDispatcher(id)))
|
||||||
|
} else {
|
||||||
|
Err(Error::NoneError)
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> Result<(), Error> {
|
fn stop(&mut self) -> Result<(), Error> {
|
||||||
trace!("stop()");
|
trace!("stop()");
|
||||||
self.0.cancel();
|
self.0.cancel()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +131,7 @@ impl Backend for SpeechDispatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_rate(&mut self, rate: f32) -> Result<(), Error> {
|
fn set_rate(&mut self, rate: f32) -> Result<(), Error> {
|
||||||
self.0.set_voice_rate(rate as i32);
|
self.0.set_voice_rate(rate as i32)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +152,7 @@ impl Backend for SpeechDispatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> {
|
fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> {
|
||||||
self.0.set_voice_pitch(pitch as i32);
|
self.0.set_voice_pitch(pitch as i32)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,11 +173,50 @@ impl Backend for SpeechDispatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_volume(&mut self, volume: f32) -> Result<(), Error> {
|
fn set_volume(&mut self, volume: f32) -> Result<(), Error> {
|
||||||
self.0.set_volume(volume as i32);
|
self.0.set_volume(volume as i32)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_speaking(&self) -> Result<bool, Error> {
|
fn is_speaking(&self) -> Result<bool, Error> {
|
||||||
|
let speaking = SPEAKING.lock().unwrap();
|
||||||
|
let is_speaking = speaking.get(&self.0.client_id()).unwrap();
|
||||||
|
Ok(*is_speaking)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn voices(&self) -> Result<Vec<Voice>, Error> {
|
||||||
|
let rv = self
|
||||||
|
.0
|
||||||
|
.list_synthesis_voices()?
|
||||||
|
.iter()
|
||||||
|
.filter(|v| LanguageTag::parse(v.language.clone()).is_ok())
|
||||||
|
.map(|v| Voice {
|
||||||
|
id: v.name.clone(),
|
||||||
|
name: v.name.clone(),
|
||||||
|
gender: None,
|
||||||
|
language: LanguageTag::parse(v.language.clone()).unwrap(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<Voice>>();
|
||||||
|
Ok(rv)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn voice(&self) -> Result<Option<Voice>, Error> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> {
|
||||||
|
for v in self.0.list_synthesis_voices()? {
|
||||||
|
if v.name == voice.name {
|
||||||
|
self.0.set_synthesis_voice(&v)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Error::OperationFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SpeechDispatcher {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut speaking = SPEAKING.lock().unwrap();
|
||||||
|
speaking.remove(&self.0.client_id());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
#[cfg(windows)]
|
#[cfg(all(windows, feature = "tolk"))]
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
use tolk::Tolk as TolkPtr;
|
use tolk::Tolk as TolkPtr;
|
||||||
|
|
||||||
use crate::{Backend, Error, Features};
|
use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice};
|
||||||
|
|
||||||
pub struct Tolk(TolkPtr);
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Tolk(Arc<TolkPtr>);
|
||||||
|
|
||||||
impl Tolk {
|
impl Tolk {
|
||||||
pub fn new() -> Option<Self> {
|
pub(crate) fn new() -> Option<Self> {
|
||||||
info!("Initializing Tolk backend");
|
info!("Initializing Tolk backend");
|
||||||
let tolk = TolkPtr::new();
|
let tolk = TolkPtr::new();
|
||||||
if tolk.detect_screen_reader().is_some() {
|
if tolk.detect_screen_reader().is_some() {
|
||||||
|
@ -19,20 +22,21 @@ impl Tolk {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend for Tolk {
|
impl Backend for Tolk {
|
||||||
|
fn id(&self) -> Option<BackendId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn supported_features(&self) -> Features {
|
fn supported_features(&self) -> Features {
|
||||||
Features {
|
Features {
|
||||||
stop: true,
|
stop: true,
|
||||||
rate: false,
|
..Default::default()
|
||||||
pitch: false,
|
|
||||||
volume: false,
|
|
||||||
is_speaking: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error> {
|
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error> {
|
||||||
trace!("speak({}, {})", text, interrupt);
|
trace!("speak({}, {})", text, interrupt);
|
||||||
self.0.speak(text, interrupt);
|
self.0.speak(text, interrupt);
|
||||||
Ok(())
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> Result<(), Error> {
|
fn stop(&mut self) -> Result<(), Error> {
|
||||||
|
@ -104,4 +108,16 @@ impl Backend for Tolk {
|
||||||
fn is_speaking(&self) -> Result<bool, Error> {
|
fn is_speaking(&self) -> Result<bool, Error> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn voice(&self) -> Result<Option<Voice>, Error> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn voices(&self) -> Result<Vec<Voice>, Error> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_voice(&mut self, _voice: &Voice) -> Result<(), Error> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,54 @@
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
use web_sys::SpeechSynthesisUtterance;
|
use oxilangtag::LanguageTag;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use web_sys::{
|
||||||
|
SpeechSynthesisErrorCode, SpeechSynthesisErrorEvent, SpeechSynthesisEvent,
|
||||||
|
SpeechSynthesisUtterance, SpeechSynthesisVoice,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{Backend, Error, Features};
|
use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct Web {
|
pub struct Web {
|
||||||
|
id: BackendId,
|
||||||
rate: f32,
|
rate: f32,
|
||||||
pitch: f32,
|
pitch: f32,
|
||||||
volume: f32,
|
volume: f32,
|
||||||
|
voice: Option<SpeechSynthesisVoice>,
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref NEXT_BACKEND_ID: Mutex<u64> = Mutex::new(0);
|
||||||
|
static ref UTTERANCE_MAPPINGS: Mutex<Vec<(BackendId, UtteranceId)>> = Mutex::new(Vec::new());
|
||||||
|
static ref NEXT_UTTERANCE_ID: Mutex<u64> = Mutex::new(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Web {
|
impl Web {
|
||||||
pub fn new() -> Result<Self, Error> {
|
pub fn new() -> Result<Self, Error> {
|
||||||
info!("Initializing Web backend");
|
info!("Initializing Web backend");
|
||||||
Ok(Web {
|
let mut backend_id = NEXT_BACKEND_ID.lock().unwrap();
|
||||||
|
let rv = Web {
|
||||||
|
id: BackendId::Web(*backend_id),
|
||||||
rate: 1.,
|
rate: 1.,
|
||||||
pitch: 1.,
|
pitch: 1.,
|
||||||
volume: 1.,
|
volume: 1.,
|
||||||
})
|
voice: None,
|
||||||
|
};
|
||||||
|
*backend_id += 1;
|
||||||
|
Ok(rv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend for Web {
|
impl Backend for Web {
|
||||||
|
fn id(&self) -> Option<BackendId> {
|
||||||
|
Some(self.id)
|
||||||
|
}
|
||||||
|
|
||||||
fn supported_features(&self) -> Features {
|
fn supported_features(&self) -> Features {
|
||||||
Features {
|
Features {
|
||||||
stop: true,
|
stop: true,
|
||||||
|
@ -29,23 +56,69 @@ impl Backend for Web {
|
||||||
pitch: true,
|
pitch: true,
|
||||||
volume: true,
|
volume: true,
|
||||||
is_speaking: true,
|
is_speaking: true,
|
||||||
|
voice: true,
|
||||||
|
get_voice: true,
|
||||||
|
utterance_callbacks: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error> {
|
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error> {
|
||||||
trace!("speak({}, {})", text, interrupt);
|
trace!("speak({}, {})", text, interrupt);
|
||||||
let utterance = SpeechSynthesisUtterance::new_with_text(text).unwrap();
|
let utterance = SpeechSynthesisUtterance::new_with_text(text).unwrap();
|
||||||
utterance.set_rate(self.rate);
|
utterance.set_rate(self.rate);
|
||||||
utterance.set_pitch(self.pitch);
|
utterance.set_pitch(self.pitch);
|
||||||
utterance.set_volume(self.volume);
|
utterance.set_volume(self.volume);
|
||||||
|
if self.voice.is_some() {
|
||||||
|
utterance.set_voice(self.voice.as_ref());
|
||||||
|
}
|
||||||
|
let id = self.id().unwrap();
|
||||||
|
let mut uid = NEXT_UTTERANCE_ID.lock().unwrap();
|
||||||
|
let utterance_id = UtteranceId::Web(*uid);
|
||||||
|
*uid += 1;
|
||||||
|
drop(uid);
|
||||||
|
let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap();
|
||||||
|
mappings.push((self.id, utterance_id));
|
||||||
|
drop(mappings);
|
||||||
|
let callback = Closure::wrap(Box::new(move |_evt: SpeechSynthesisEvent| {
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let callback = callbacks.get_mut(&id).unwrap();
|
||||||
|
if let Some(f) = callback.utterance_begin.as_mut() {
|
||||||
|
f(utterance_id);
|
||||||
|
}
|
||||||
|
}) as Box<dyn Fn(_)>);
|
||||||
|
utterance.set_onstart(Some(callback.as_ref().unchecked_ref()));
|
||||||
|
let callback = Closure::wrap(Box::new(move |_evt: SpeechSynthesisEvent| {
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let callback = callbacks.get_mut(&id).unwrap();
|
||||||
|
if let Some(f) = callback.utterance_end.as_mut() {
|
||||||
|
f(utterance_id);
|
||||||
|
}
|
||||||
|
let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap();
|
||||||
|
mappings.retain(|v| v.1 != utterance_id);
|
||||||
|
}) as Box<dyn Fn(_)>);
|
||||||
|
utterance.set_onend(Some(callback.as_ref().unchecked_ref()));
|
||||||
|
let callback = Closure::wrap(Box::new(move |evt: SpeechSynthesisErrorEvent| {
|
||||||
|
if evt.error() == SpeechSynthesisErrorCode::Canceled {
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let callback = callbacks.get_mut(&id).unwrap();
|
||||||
|
if let Some(f) = callback.utterance_stop.as_mut() {
|
||||||
|
f(utterance_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap();
|
||||||
|
mappings.retain(|v| v.1 != utterance_id);
|
||||||
|
}) as Box<dyn Fn(_)>);
|
||||||
|
utterance.set_onerror(Some(callback.as_ref().unchecked_ref()));
|
||||||
if interrupt {
|
if interrupt {
|
||||||
self.stop()?;
|
self.stop()?;
|
||||||
}
|
}
|
||||||
if let Some(window) = web_sys::window() {
|
if let Some(window) = web_sys::window() {
|
||||||
let speech_synthesis = window.speech_synthesis().unwrap();
|
let speech_synthesis = window.speech_synthesis().unwrap();
|
||||||
speech_synthesis.speak(&utterance);
|
speech_synthesis.speak(&utterance);
|
||||||
|
Ok(Some(utterance_id))
|
||||||
|
} else {
|
||||||
|
Err(Error::NoneError)
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> Result<(), Error> {
|
fn stop(&mut self) -> Result<(), Error> {
|
||||||
|
@ -131,4 +204,72 @@ impl Backend for Web {
|
||||||
Err(Error::NoneError)
|
Err(Error::NoneError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn voice(&self) -> Result<Option<Voice>, Error> {
|
||||||
|
if let Some(voice) = &self.voice {
|
||||||
|
Ok(Some(voice.clone().into()))
|
||||||
|
} else {
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
let speech_synthesis = window.speech_synthesis().unwrap();
|
||||||
|
for voice in speech_synthesis.get_voices().iter() {
|
||||||
|
let voice: SpeechSynthesisVoice = voice.into();
|
||||||
|
if voice.default() {
|
||||||
|
return Ok(Some(voice.into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::NoneError);
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn voices(&self) -> Result<Vec<Voice>, Error> {
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
let speech_synthesis = window.speech_synthesis().unwrap();
|
||||||
|
let mut rv: Vec<Voice> = vec![];
|
||||||
|
for v in speech_synthesis.get_voices().iter() {
|
||||||
|
let v: SpeechSynthesisVoice = v.into();
|
||||||
|
rv.push(v.into());
|
||||||
|
}
|
||||||
|
Ok(rv)
|
||||||
|
} else {
|
||||||
|
Err(Error::NoneError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> {
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
let speech_synthesis = window.speech_synthesis().unwrap();
|
||||||
|
for v in speech_synthesis.get_voices().iter() {
|
||||||
|
let v: SpeechSynthesisVoice = v.into();
|
||||||
|
if v.voice_uri() == voice.id {
|
||||||
|
self.voice = Some(v);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Error::OperationFailed)
|
||||||
|
} else {
|
||||||
|
Err(Error::NoneError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Web {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap();
|
||||||
|
mappings.retain(|v| v.0 != self.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SpeechSynthesisVoice> for Voice {
|
||||||
|
fn from(other: SpeechSynthesisVoice) -> Self {
|
||||||
|
let language = LanguageTag::parse(other.lang()).unwrap();
|
||||||
|
Voice {
|
||||||
|
id: other.voice_uri(),
|
||||||
|
name: other.name(),
|
||||||
|
gender: None,
|
||||||
|
language,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,150 @@
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use log::{info, trace};
|
use std::{
|
||||||
|
collections::{HashMap, VecDeque},
|
||||||
use tts_winrt_bindings::windows::media::core::MediaSource;
|
sync::Mutex,
|
||||||
use tts_winrt_bindings::windows::media::playback::{
|
|
||||||
MediaPlaybackItem, MediaPlaybackList, MediaPlaybackState, MediaPlayer,
|
|
||||||
};
|
};
|
||||||
use tts_winrt_bindings::windows::media::speech_synthesis::SpeechSynthesizer;
|
|
||||||
|
|
||||||
use crate::{Backend, Error, Features};
|
use lazy_static::lazy_static;
|
||||||
|
use log::{info, trace};
|
||||||
|
use oxilangtag::LanguageTag;
|
||||||
|
use windows::{
|
||||||
|
Foundation::TypedEventHandler,
|
||||||
|
Media::{
|
||||||
|
Core::MediaSource,
|
||||||
|
Playback::{MediaPlayer, MediaPlayerAudioCategory},
|
||||||
|
SpeechSynthesis::{SpeechSynthesizer, VoiceGender, VoiceInformation},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
impl From<winrt::Error> for Error {
|
use crate::{Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, CALLBACKS};
|
||||||
fn from(e: winrt::Error) -> Self {
|
|
||||||
Error::WinRT(e)
|
impl From<windows::core::Error> for Error {
|
||||||
|
fn from(e: windows::core::Error) -> Self {
|
||||||
|
Error::WinRt(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WinRT {
|
#[derive(Clone)]
|
||||||
|
pub struct WinRt {
|
||||||
|
id: BackendId,
|
||||||
synth: SpeechSynthesizer,
|
synth: SpeechSynthesizer,
|
||||||
player: MediaPlayer,
|
player: MediaPlayer,
|
||||||
playback_list: MediaPlaybackList,
|
rate: f32,
|
||||||
|
pitch: f32,
|
||||||
|
volume: f32,
|
||||||
|
voice: VoiceInformation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WinRT {
|
struct Utterance {
|
||||||
|
id: UtteranceId,
|
||||||
|
text: String,
|
||||||
|
rate: f32,
|
||||||
|
pitch: f32,
|
||||||
|
volume: f32,
|
||||||
|
voice: VoiceInformation,
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref NEXT_BACKEND_ID: Mutex<u64> = Mutex::new(0);
|
||||||
|
static ref NEXT_UTTERANCE_ID: Mutex<u64> = Mutex::new(0);
|
||||||
|
static ref BACKEND_TO_SPEECH_SYNTHESIZER: Mutex<HashMap<BackendId, SpeechSynthesizer>> = {
|
||||||
|
let v: HashMap<BackendId, SpeechSynthesizer> = HashMap::new();
|
||||||
|
Mutex::new(v)
|
||||||
|
};
|
||||||
|
static ref BACKEND_TO_MEDIA_PLAYER: Mutex<HashMap<BackendId, MediaPlayer>> = {
|
||||||
|
let v: HashMap<BackendId, MediaPlayer> = HashMap::new();
|
||||||
|
Mutex::new(v)
|
||||||
|
};
|
||||||
|
static ref UTTERANCES: Mutex<HashMap<BackendId, VecDeque<Utterance>>> = {
|
||||||
|
let utterances: HashMap<BackendId, VecDeque<Utterance>> = HashMap::new();
|
||||||
|
Mutex::new(utterances)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WinRt {
|
||||||
pub fn new() -> std::result::Result<Self, Error> {
|
pub fn new() -> std::result::Result<Self, Error> {
|
||||||
info!("Initializing WinRT backend");
|
info!("Initializing WinRT backend");
|
||||||
let playback_list = MediaPlaybackList::new()?;
|
let synth = SpeechSynthesizer::new()?;
|
||||||
let player = MediaPlayer::new()?;
|
let player = MediaPlayer::new()?;
|
||||||
player.set_auto_play(true)?;
|
player.SetRealTimePlayback(true)?;
|
||||||
player.set_source(&playback_list)?;
|
player.SetAudioCategory(MediaPlayerAudioCategory::Speech)?;
|
||||||
|
let mut backend_id = NEXT_BACKEND_ID.lock().unwrap();
|
||||||
|
let bid = BackendId::WinRt(*backend_id);
|
||||||
|
*backend_id += 1;
|
||||||
|
drop(backend_id);
|
||||||
|
{
|
||||||
|
let mut utterances = UTTERANCES.lock().unwrap();
|
||||||
|
utterances.insert(bid, VecDeque::new());
|
||||||
|
}
|
||||||
|
let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap();
|
||||||
|
backend_to_media_player.insert(bid, player.clone());
|
||||||
|
drop(backend_to_media_player);
|
||||||
|
let mut backend_to_speech_synthesizer = BACKEND_TO_SPEECH_SYNTHESIZER.lock().unwrap();
|
||||||
|
backend_to_speech_synthesizer.insert(bid, synth.clone());
|
||||||
|
drop(backend_to_speech_synthesizer);
|
||||||
|
let bid_clone = bid;
|
||||||
|
player.MediaEnded(&TypedEventHandler::new(
|
||||||
|
move |sender: &Option<MediaPlayer>, _args| {
|
||||||
|
if let Some(sender) = sender {
|
||||||
|
let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap();
|
||||||
|
let id = backend_to_media_player.iter().find(|v| v.1 == sender);
|
||||||
|
if let Some((id, _)) = id {
|
||||||
|
let mut utterances = UTTERANCES.lock().unwrap();
|
||||||
|
if let Some(utterances) = utterances.get_mut(id) {
|
||||||
|
if let Some(utterance) = utterances.pop_front() {
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let callbacks = callbacks.get_mut(id).unwrap();
|
||||||
|
if let Some(callback) = callbacks.utterance_end.as_mut() {
|
||||||
|
callback(utterance.id);
|
||||||
|
}
|
||||||
|
if let Some(utterance) = utterances.front() {
|
||||||
|
let backend_to_speech_synthesizer =
|
||||||
|
BACKEND_TO_SPEECH_SYNTHESIZER.lock().unwrap();
|
||||||
|
let id = backend_to_speech_synthesizer
|
||||||
|
.iter()
|
||||||
|
.find(|v| *v.0 == bid_clone);
|
||||||
|
if let Some((_, tts)) = id {
|
||||||
|
tts.Options()?.SetSpeakingRate(utterance.rate.into())?;
|
||||||
|
tts.Options()?.SetAudioPitch(utterance.pitch.into())?;
|
||||||
|
tts.Options()?.SetAudioVolume(utterance.volume.into())?;
|
||||||
|
tts.SetVoice(&utterance.voice)?;
|
||||||
|
let text = &utterance.text;
|
||||||
|
let stream =
|
||||||
|
tts.SynthesizeTextToStreamAsync(&text.into())?.get()?;
|
||||||
|
let content_type = stream.ContentType()?;
|
||||||
|
let source =
|
||||||
|
MediaSource::CreateFromStream(&stream, &content_type)?;
|
||||||
|
sender.SetSource(&source)?;
|
||||||
|
sender.Play()?;
|
||||||
|
if let Some(callback) = callbacks.utterance_begin.as_mut() {
|
||||||
|
callback(utterance.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
))?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
synth: SpeechSynthesizer::new()?,
|
id: bid,
|
||||||
player: player,
|
synth,
|
||||||
playback_list: playback_list,
|
player,
|
||||||
|
rate: 1.,
|
||||||
|
pitch: 1.,
|
||||||
|
volume: 1.,
|
||||||
|
voice: SpeechSynthesizer::DefaultVoice()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reinit_player(&mut self) -> std::result::Result<(), Error> {
|
|
||||||
self.playback_list = MediaPlaybackList::new()?;
|
|
||||||
self.player = MediaPlayer::new()?;
|
|
||||||
self.player.set_auto_play(true)?;
|
|
||||||
self.player.set_source(&self.playback_list)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend for WinRT {
|
impl Backend for WinRt {
|
||||||
|
fn id(&self) -> Option<BackendId> {
|
||||||
|
Some(self.id)
|
||||||
|
}
|
||||||
|
|
||||||
fn supported_features(&self) -> Features {
|
fn supported_features(&self) -> Features {
|
||||||
Features {
|
Features {
|
||||||
stop: true,
|
stop: true,
|
||||||
|
@ -52,36 +152,83 @@ impl Backend for WinRT {
|
||||||
pitch: true,
|
pitch: true,
|
||||||
volume: true,
|
volume: true,
|
||||||
is_speaking: true,
|
is_speaking: true,
|
||||||
|
voice: true,
|
||||||
|
get_voice: true,
|
||||||
|
utterance_callbacks: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn speak(&mut self, text: &str, interrupt: bool) -> std::result::Result<(), Error> {
|
fn speak(
|
||||||
trace!("speak({}, {})", text, interrupt);
|
&mut self,
|
||||||
if interrupt {
|
text: &str,
|
||||||
|
interrupt: bool,
|
||||||
|
) -> std::result::Result<Option<UtteranceId>, Error> {
|
||||||
|
if interrupt && self.is_speaking()? {
|
||||||
self.stop()?;
|
self.stop()?;
|
||||||
}
|
}
|
||||||
let stream = self.synth.synthesize_text_to_stream_async(text)?.get()?;
|
let utterance_id = {
|
||||||
let content_type = stream.content_type()?;
|
let mut uid = NEXT_UTTERANCE_ID.lock().unwrap();
|
||||||
let source = MediaSource::create_from_stream(stream, content_type)?;
|
let utterance_id = UtteranceId::WinRt(*uid);
|
||||||
let item = MediaPlaybackItem::create(source)?;
|
*uid += 1;
|
||||||
let state = self.player.playback_session()?.playback_state()?;
|
utterance_id
|
||||||
if state == MediaPlaybackState::Paused {
|
};
|
||||||
let index = self.playback_list.current_item_index()?;
|
let mut no_utterances = false;
|
||||||
let total = self.playback_list.items()?.size()?;
|
{
|
||||||
if total != 0 && index == total - 1 {
|
let mut utterances = UTTERANCES.lock().unwrap();
|
||||||
self.reinit_player()?;
|
if let Some(utterances) = utterances.get_mut(&self.id) {
|
||||||
|
no_utterances = utterances.is_empty();
|
||||||
|
let utterance = Utterance {
|
||||||
|
id: utterance_id,
|
||||||
|
text: text.into(),
|
||||||
|
rate: self.rate,
|
||||||
|
pitch: self.pitch,
|
||||||
|
volume: self.volume,
|
||||||
|
voice: self.voice.clone(),
|
||||||
|
};
|
||||||
|
utterances.push_back(utterance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.playback_list.items()?.append(item)?;
|
if no_utterances {
|
||||||
if !self.is_speaking()? {
|
self.synth.Options()?.SetSpeakingRate(self.rate.into())?;
|
||||||
self.player.play()?;
|
self.synth.Options()?.SetAudioPitch(self.pitch.into())?;
|
||||||
|
self.synth.Options()?.SetAudioVolume(self.volume.into())?;
|
||||||
|
self.synth.SetVoice(&self.voice)?;
|
||||||
|
let stream = self
|
||||||
|
.synth
|
||||||
|
.SynthesizeTextToStreamAsync(&text.into())?
|
||||||
|
.get()?;
|
||||||
|
let content_type = stream.ContentType()?;
|
||||||
|
let source = MediaSource::CreateFromStream(&stream, &content_type)?;
|
||||||
|
self.player.SetSource(&source)?;
|
||||||
|
self.player.Play()?;
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let callbacks = callbacks.get_mut(&self.id).unwrap();
|
||||||
|
if let Some(callback) = callbacks.utterance_begin.as_mut() {
|
||||||
|
callback(utterance_id);
|
||||||
}
|
}
|
||||||
Ok(())
|
}
|
||||||
|
Ok(Some(utterance_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> std::result::Result<(), Error> {
|
fn stop(&mut self) -> std::result::Result<(), Error> {
|
||||||
trace!("stop()");
|
trace!("stop()");
|
||||||
self.reinit_player()?;
|
if !self.is_speaking()? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut utterances = UTTERANCES.lock().unwrap();
|
||||||
|
if let Some(utterances) = utterances.get(&self.id) {
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let callbacks = callbacks.get_mut(&self.id).unwrap();
|
||||||
|
if let Some(callback) = callbacks.utterance_stop.as_mut() {
|
||||||
|
for utterance in utterances {
|
||||||
|
callback(utterance.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(utterances) = utterances.get_mut(&self.id) {
|
||||||
|
utterances.clear();
|
||||||
|
}
|
||||||
|
self.player.Pause()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,12 +245,12 @@ impl Backend for WinRT {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_rate(&self) -> std::result::Result<f32, Error> {
|
fn get_rate(&self) -> std::result::Result<f32, Error> {
|
||||||
let rate = self.synth.options()?.speaking_rate()?;
|
let rate = self.synth.Options()?.SpeakingRate()?;
|
||||||
Ok(rate as f32)
|
Ok(rate as f32)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_rate(&mut self, rate: f32) -> std::result::Result<(), Error> {
|
fn set_rate(&mut self, rate: f32) -> std::result::Result<(), Error> {
|
||||||
self.synth.options()?.set_speaking_rate(rate.into())?;
|
self.rate = rate;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,12 +267,12 @@ impl Backend for WinRT {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_pitch(&self) -> std::result::Result<f32, Error> {
|
fn get_pitch(&self) -> std::result::Result<f32, Error> {
|
||||||
let pitch = self.synth.options()?.audio_pitch()?;
|
let pitch = self.synth.Options()?.AudioPitch()?;
|
||||||
Ok(pitch as f32)
|
Ok(pitch as f32)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_pitch(&mut self, pitch: f32) -> std::result::Result<(), Error> {
|
fn set_pitch(&mut self, pitch: f32) -> std::result::Result<(), Error> {
|
||||||
self.synth.options()?.set_audio_pitch(pitch.into())?;
|
self.pitch = pitch;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,18 +289,76 @@ impl Backend for WinRT {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_volume(&self) -> std::result::Result<f32, Error> {
|
fn get_volume(&self) -> std::result::Result<f32, Error> {
|
||||||
let volume = self.synth.options()?.audio_volume()?;
|
let volume = self.synth.Options()?.AudioVolume()?;
|
||||||
Ok(volume as f32)
|
Ok(volume as f32)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_volume(&mut self, volume: f32) -> std::result::Result<(), Error> {
|
fn set_volume(&mut self, volume: f32) -> std::result::Result<(), Error> {
|
||||||
self.synth.options()?.set_audio_volume(volume.into())?;
|
self.volume = volume;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_speaking(&self) -> std::result::Result<bool, Error> {
|
fn is_speaking(&self) -> std::result::Result<bool, Error> {
|
||||||
let state = self.player.playback_session()?.playback_state()?;
|
let utterances = UTTERANCES.lock().unwrap();
|
||||||
let playing = state == MediaPlaybackState::Opening || state == MediaPlaybackState::Playing;
|
let utterances = utterances.get(&self.id).unwrap();
|
||||||
Ok(playing)
|
Ok(!utterances.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn voice(&self) -> Result<Option<Voice>, Error> {
|
||||||
|
let voice = self.synth.Voice()?;
|
||||||
|
let voice = voice.try_into()?;
|
||||||
|
Ok(Some(voice))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn voices(&self) -> Result<Vec<Voice>, Error> {
|
||||||
|
let mut rv: Vec<Voice> = vec![];
|
||||||
|
for voice in SpeechSynthesizer::AllVoices()? {
|
||||||
|
rv.push(voice.try_into()?);
|
||||||
|
}
|
||||||
|
Ok(rv)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> {
|
||||||
|
for v in SpeechSynthesizer::AllVoices()? {
|
||||||
|
let vid: String = v.Id()?.try_into()?;
|
||||||
|
if vid == voice.id {
|
||||||
|
self.voice = v;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Error::OperationFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for WinRt {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let id = self.id;
|
||||||
|
let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap();
|
||||||
|
backend_to_media_player.remove(&id);
|
||||||
|
let mut backend_to_speech_synthesizer = BACKEND_TO_SPEECH_SYNTHESIZER.lock().unwrap();
|
||||||
|
backend_to_speech_synthesizer.remove(&id);
|
||||||
|
let mut utterances = UTTERANCES.lock().unwrap();
|
||||||
|
utterances.remove(&id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryInto<Voice> for VoiceInformation {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_into(self) -> Result<Voice, Self::Error> {
|
||||||
|
let gender = self.Gender()?;
|
||||||
|
let gender = if gender == VoiceGender::Male {
|
||||||
|
Gender::Male
|
||||||
|
} else {
|
||||||
|
Gender::Female
|
||||||
|
};
|
||||||
|
let language: String = self.Language()?.try_into()?;
|
||||||
|
let language = LanguageTag::parse(language).unwrap();
|
||||||
|
Ok(Voice {
|
||||||
|
id: self.Id()?.try_into()?,
|
||||||
|
name: self.DisplayName()?.try_into()?,
|
||||||
|
gender: Some(gender),
|
||||||
|
language,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
605
src/lib.rs
605
src/lib.rs
|
@ -1,74 +1,223 @@
|
||||||
/*!
|
//! * a Text-To-Speech (TTS) library providing high-level interfaces to a variety of backends.
|
||||||
* a Text-To-Speech (TTS) library providing high-level interfaces to a variety of backends.
|
//! * Currently supported backends are:
|
||||||
* Currently supported backends are:
|
//! * * Windows
|
||||||
* * Windows
|
//! * * Screen readers/SAPI via Tolk (requires `tolk` Cargo feature)
|
||||||
* * Screen readers/SAPI via Tolk
|
//! * * WinRT
|
||||||
* * WinRT
|
//! * * Linux via [Speech Dispatcher](https://freebsoft.org/speechd)
|
||||||
* * Linux via [Speech Dispatcher](https://freebsoft.org/speechd)
|
//! * * MacOS/iOS
|
||||||
* * MacOS
|
//! * * AppKit on MacOS 10.13 and below
|
||||||
* * AppKit on MacOS 10.13 and below
|
//! * * AVFoundation on MacOS 10.14 and above, and iOS
|
||||||
* * AVFoundation on MacOS 10.14 and above, and iOS
|
//! * * Android
|
||||||
* * WebAssembly
|
//! * * WebAssembly
|
||||||
*/
|
|
||||||
|
|
||||||
use std::boxed::Box;
|
use std::collections::HashMap;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use std::ffi::CStr;
|
use std::ffi::CStr;
|
||||||
|
use std::fmt;
|
||||||
|
use std::rc::Rc;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use std::string::FromUtf16Error;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::{boxed::Box, sync::RwLock};
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
use cocoa_foundation::base::id;
|
use cocoa_foundation::base::id;
|
||||||
|
use dyn_clonable::*;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use libc::c_char;
|
use libc::c_char;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use objc::{class, msg_send, sel, sel_impl};
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
|
pub use oxilangtag::LanguageTag;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use speech_dispatcher::Error as SpeechDispatcherError;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
#[cfg(all(windows, feature = "tolk"))]
|
||||||
|
use tolk::Tolk;
|
||||||
|
|
||||||
mod backends;
|
mod backends;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub enum Backends {
|
pub enum Backends {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "android")]
|
||||||
SpeechDispatcher,
|
Android,
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
Web,
|
|
||||||
#[cfg(windows)]
|
|
||||||
Tolk,
|
|
||||||
#[cfg(windows)]
|
|
||||||
WinRT,
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
AppKit,
|
AppKit,
|
||||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
AvFoundation,
|
AvFoundation,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
SpeechDispatcher,
|
||||||
|
#[cfg(all(windows, feature = "tolk"))]
|
||||||
|
Tolk,
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
Web,
|
||||||
|
#[cfg(windows)]
|
||||||
|
WinRt,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Backends {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||||
|
match self {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
Backends::Android => writeln!(f, "Android"),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
Backends::AppKit => writeln!(f, "AppKit"),
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
|
Backends::AvFoundation => writeln!(f, "AVFoundation"),
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
Backends::SpeechDispatcher => writeln!(f, "Speech Dispatcher"),
|
||||||
|
#[cfg(all(windows, feature = "tolk"))]
|
||||||
|
Backends::Tolk => writeln!(f, "Tolk"),
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
Backends::Web => writeln!(f, "Web"),
|
||||||
|
#[cfg(windows)]
|
||||||
|
Backends::WinRt => writeln!(f, "Windows Runtime"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub enum BackendId {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
Android(u64),
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
|
AvFoundation(u64),
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
SpeechDispatcher(usize),
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
Web(u64),
|
||||||
|
#[cfg(windows)]
|
||||||
|
WinRt(u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for BackendId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||||
|
match self {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
BackendId::Android(id) => writeln!(f, "Android({id})"),
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
|
BackendId::AvFoundation(id) => writeln!(f, "AvFoundation({id})"),
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
BackendId::SpeechDispatcher(id) => writeln!(f, "SpeechDispatcher({id})"),
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
BackendId::Web(id) => writeln!(f, "Web({id})"),
|
||||||
|
#[cfg(windows)]
|
||||||
|
BackendId::WinRt(id) => writeln!(f, "WinRT({id})"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// # Note
|
||||||
|
//
|
||||||
|
// Most trait implementations are blocked by cocoa_foundation::base::id;
|
||||||
|
// which is a type alias for objc::runtime::Object, which only implements Debug.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[cfg_attr(
|
||||||
|
not(any(target_os = "macos", target_os = "ios")),
|
||||||
|
derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)
|
||||||
|
)]
|
||||||
|
#[cfg_attr(
|
||||||
|
all(feature = "serde", not(any(target_os = "macos", target_os = "ios"))),
|
||||||
|
derive(serde::Serialize, serde::Deserialize)
|
||||||
|
)]
|
||||||
|
pub enum UtteranceId {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
Android(u64),
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
|
AvFoundation(id),
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
SpeechDispatcher(u64),
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
Web(u64),
|
||||||
|
#[cfg(windows)]
|
||||||
|
WinRt(u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
// # Note
|
||||||
|
//
|
||||||
|
// Display is not implemented by cocoa_foundation::base::id;
|
||||||
|
// which is a type alias for objc::runtime::Object, which only implements Debug.
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
|
||||||
|
impl fmt::Display for UtteranceId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||||
|
match self {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
UtteranceId::Android(id) => writeln!(f, "Android({id})"),
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
UtteranceId::SpeechDispatcher(id) => writeln!(f, "SpeechDispatcher({id})"),
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
UtteranceId::Web(id) => writeln!(f, "Web({})", id),
|
||||||
|
#[cfg(windows)]
|
||||||
|
UtteranceId::WinRt(id) => writeln!(f, "WinRt({id})"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for UtteranceId {}
|
||||||
|
|
||||||
|
unsafe impl Sync for UtteranceId {}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Features {
|
pub struct Features {
|
||||||
pub stop: bool,
|
|
||||||
pub rate: bool,
|
|
||||||
pub pitch: bool,
|
|
||||||
pub volume: bool,
|
|
||||||
pub is_speaking: bool,
|
pub is_speaking: bool,
|
||||||
|
pub pitch: bool,
|
||||||
|
pub rate: bool,
|
||||||
|
pub stop: bool,
|
||||||
|
pub utterance_callbacks: bool,
|
||||||
|
pub voice: bool,
|
||||||
|
pub get_voice: bool,
|
||||||
|
pub volume: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Features {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||||
|
writeln!(f, "{self:#?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Features {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
IO(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
#[error("Value not received")]
|
#[error("Value not received")]
|
||||||
NoneError,
|
NoneError,
|
||||||
|
#[error("Operation failed")]
|
||||||
|
OperationFailed,
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
#[error("JavaScript error: [0])]")]
|
#[error("JavaScript error: [0]")]
|
||||||
JavaScriptError(wasm_bindgen::JsValue),
|
JavaScriptError(wasm_bindgen::JsValue),
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[error("Speech Dispatcher error: {0}")]
|
||||||
|
SpeechDispatcher(#[from] SpeechDispatcherError),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
#[error("WinRT error")]
|
#[error("WinRT error")]
|
||||||
WinRT(winrt::Error),
|
WinRt(windows::core::Error),
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[error("UTF string conversion failed")]
|
||||||
|
UtfStringConversionFailed(#[from] FromUtf16Error),
|
||||||
#[error("Unsupported feature")]
|
#[error("Unsupported feature")]
|
||||||
UnsupportedFeature,
|
UnsupportedFeature,
|
||||||
#[error("Out of range")]
|
#[error("Out of range")]
|
||||||
OutOfRange,
|
OutOfRange,
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[error("JNI error: [0])]")]
|
||||||
|
JNI(#[from] jni::errors::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Backend {
|
#[clonable]
|
||||||
|
pub trait Backend: Clone {
|
||||||
|
fn id(&self) -> Option<BackendId>;
|
||||||
fn supported_features(&self) -> Features;
|
fn supported_features(&self) -> Features;
|
||||||
fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error>;
|
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error>;
|
||||||
fn stop(&mut self) -> Result<(), Error>;
|
fn stop(&mut self) -> Result<(), Error>;
|
||||||
fn min_rate(&self) -> f32;
|
fn min_rate(&self) -> f32;
|
||||||
fn max_rate(&self) -> f32;
|
fn max_rate(&self) -> f32;
|
||||||
|
@ -86,59 +235,103 @@ pub trait Backend {
|
||||||
fn get_volume(&self) -> Result<f32, Error>;
|
fn get_volume(&self) -> Result<f32, Error>;
|
||||||
fn set_volume(&mut self, volume: f32) -> Result<(), Error>;
|
fn set_volume(&mut self, volume: f32) -> Result<(), Error>;
|
||||||
fn is_speaking(&self) -> Result<bool, Error>;
|
fn is_speaking(&self) -> Result<bool, Error>;
|
||||||
|
fn voices(&self) -> Result<Vec<Voice>, Error>;
|
||||||
|
fn voice(&self) -> Result<Option<Voice>, Error>;
|
||||||
|
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TTS(Box<dyn Backend>);
|
#[derive(Default)]
|
||||||
|
struct Callbacks {
|
||||||
|
utterance_begin: Option<Box<dyn FnMut(UtteranceId)>>,
|
||||||
|
utterance_end: Option<Box<dyn FnMut(UtteranceId)>>,
|
||||||
|
utterance_stop: Option<Box<dyn FnMut(UtteranceId)>>,
|
||||||
|
}
|
||||||
|
|
||||||
unsafe impl std::marker::Send for TTS {}
|
unsafe impl Send for Callbacks {}
|
||||||
|
|
||||||
unsafe impl std::marker::Sync for TTS {}
|
unsafe impl Sync for Callbacks {}
|
||||||
|
|
||||||
impl TTS {
|
lazy_static! {
|
||||||
/**
|
static ref CALLBACKS: Mutex<HashMap<BackendId, Callbacks>> = {
|
||||||
* Create a new `TTS` instance with the specified backend.
|
let m: HashMap<BackendId, Callbacks> = HashMap::new();
|
||||||
*/
|
Mutex::new(m)
|
||||||
pub fn new(backend: Backends) -> Result<TTS, Error> {
|
};
|
||||||
match backend {
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Tts(Rc<RwLock<Box<dyn Backend>>>);
|
||||||
|
|
||||||
|
unsafe impl Send for Tts {}
|
||||||
|
|
||||||
|
unsafe impl Sync for Tts {}
|
||||||
|
|
||||||
|
impl Tts {
|
||||||
|
/// Create a new `TTS` instance with the specified backend.
|
||||||
|
pub fn new(backend: Backends) -> Result<Tts, Error> {
|
||||||
|
let backend = match backend {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
Backends::SpeechDispatcher => Ok(TTS(Box::new(backends::SpeechDispatcher::new()))),
|
Backends::SpeechDispatcher => {
|
||||||
|
let tts = backends::SpeechDispatcher::new()?;
|
||||||
|
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
|
||||||
|
}
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
Backends::Web => {
|
Backends::Web => {
|
||||||
let tts = backends::Web::new()?;
|
let tts = backends::Web::new()?;
|
||||||
Ok(TTS(Box::new(tts)))
|
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(all(windows, feature = "tolk"))]
|
||||||
Backends::Tolk => {
|
Backends::Tolk => {
|
||||||
let tts = backends::Tolk::new();
|
let tts = backends::Tolk::new();
|
||||||
if let Some(tts) = tts {
|
if let Some(tts) = tts {
|
||||||
Ok(TTS(Box::new(tts)))
|
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
|
||||||
} else {
|
} else {
|
||||||
Err(Error::NoneError)
|
Err(Error::NoneError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
Backends::WinRT => {
|
Backends::WinRt => {
|
||||||
let tts = backends::winrt::WinRT::new()?;
|
let tts = backends::WinRt::new()?;
|
||||||
Ok(TTS(Box::new(tts)))
|
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
Backends::AppKit => Ok(TTS(Box::new(backends::AppKit::new()))),
|
Backends::AppKit => Ok(Tts(Rc::new(RwLock::new(
|
||||||
|
Box::new(backends::AppKit::new()?),
|
||||||
|
)))),
|
||||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
Backends::AvFoundation => Ok(TTS(Box::new(backends::AvFoundation::new()))),
|
Backends::AvFoundation => Ok(Tts(Rc::new(RwLock::new(Box::new(
|
||||||
|
backends::AvFoundation::new()?,
|
||||||
|
))))),
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
Backends::Android => {
|
||||||
|
let tts = backends::Android::new()?;
|
||||||
|
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Ok(backend) = backend {
|
||||||
|
if let Some(id) = backend.0.read().unwrap().id() {
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
callbacks.insert(id, Callbacks::default());
|
||||||
|
}
|
||||||
|
Ok(backend)
|
||||||
|
} else {
|
||||||
|
backend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default() -> Result<TTS, Error> {
|
#[allow(clippy::should_implement_trait)]
|
||||||
|
pub fn default() -> Result<Tts, Error> {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let tts = TTS::new(Backends::SpeechDispatcher);
|
let tts = Tts::new(Backends::SpeechDispatcher);
|
||||||
#[cfg(windows)]
|
#[cfg(all(windows, feature = "tolk"))]
|
||||||
let tts = if let Some(tts) = TTS::new(Backends::Tolk).ok() {
|
let tts = if let Ok(tts) = Tts::new(Backends::Tolk) {
|
||||||
Ok(tts)
|
Ok(tts)
|
||||||
} else {
|
} else {
|
||||||
TTS::new(Backends::WinRT)
|
Tts::new(Backends::WinRt)
|
||||||
};
|
};
|
||||||
|
#[cfg(all(windows, not(feature = "tolk")))]
|
||||||
|
let tts = Tts::new(Backends::WinRt);
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let tts = TTS::new(Backends::Web);
|
let tts = Tts::new(Backends::Web);
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let tts = unsafe {
|
let tts = unsafe {
|
||||||
// Needed because the Rust NSProcessInfo structs report bogus values, and I don't want to pull in a full bindgen stack.
|
// Needed because the Rust NSProcessInfo structs report bogus values, and I don't want to pull in a full bindgen stack.
|
||||||
|
@ -147,94 +340,88 @@ impl TTS {
|
||||||
let str: *const c_char = msg_send![version, UTF8String];
|
let str: *const c_char = msg_send![version, UTF8String];
|
||||||
let str = CStr::from_ptr(str);
|
let str = CStr::from_ptr(str);
|
||||||
let str = str.to_string_lossy();
|
let str = str.to_string_lossy();
|
||||||
let version: Vec<&str> = str.split(" ").collect();
|
let version: Vec<&str> = str.split(' ').collect();
|
||||||
let version = version[1];
|
let version = version[1];
|
||||||
let version_parts: Vec<&str> = version.split(".").collect();
|
let version_parts: Vec<&str> = version.split('.').collect();
|
||||||
|
let major_version: i8 = version_parts[0].parse().unwrap();
|
||||||
let minor_version: i8 = version_parts[1].parse().unwrap();
|
let minor_version: i8 = version_parts[1].parse().unwrap();
|
||||||
if minor_version >= 14 {
|
if major_version >= 11 || minor_version >= 14 {
|
||||||
TTS::new(Backends::AvFoundation)
|
Tts::new(Backends::AvFoundation)
|
||||||
} else {
|
} else {
|
||||||
TTS::new(Backends::AppKit)
|
Tts::new(Backends::AppKit)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
let tts = TTS::new(Backends::AvFoundation);
|
let tts = Tts::new(Backends::AvFoundation);
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
let tts = Tts::new(Backends::Android);
|
||||||
tts
|
tts
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Returns the features supported by this TTS engine
|
||||||
* Returns the features supported by this TTS engine
|
|
||||||
*/
|
|
||||||
pub fn supported_features(&self) -> Features {
|
pub fn supported_features(&self) -> Features {
|
||||||
self.0.supported_features()
|
self.0.read().unwrap().supported_features()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Speaks the specified text, optionally interrupting current speech.
|
||||||
* Speaks the specified text, optionally interrupting current speech.
|
pub fn speak<S: Into<String>>(
|
||||||
*/
|
&mut self,
|
||||||
pub fn speak<S: Into<String>>(&mut self, text: S, interrupt: bool) -> Result<&Self, Error> {
|
text: S,
|
||||||
self.0.speak(text.into().as_str(), interrupt)?;
|
interrupt: bool,
|
||||||
Ok(self)
|
) -> Result<Option<UtteranceId>, Error> {
|
||||||
|
self.0
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.speak(text.into().as_str(), interrupt)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Stops current speech.
|
||||||
* Stops current speech.
|
|
||||||
*/
|
|
||||||
pub fn stop(&mut self) -> Result<&Self, Error> {
|
pub fn stop(&mut self) -> Result<&Self, Error> {
|
||||||
let Features { stop, .. } = self.supported_features();
|
let Features { stop, .. } = self.supported_features();
|
||||||
if stop {
|
if stop {
|
||||||
self.0.stop()?;
|
self.0.write().unwrap().stop()?;
|
||||||
Ok(self)
|
Ok(self)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::UnsupportedFeature)
|
Err(Error::UnsupportedFeature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Returns the minimum rate for this speech synthesizer.
|
||||||
* Returns the minimum rate for this speech synthesizer.
|
|
||||||
*/
|
|
||||||
pub fn min_rate(&self) -> f32 {
|
pub fn min_rate(&self) -> f32 {
|
||||||
self.0.min_rate()
|
self.0.read().unwrap().min_rate()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Returns the maximum rate for this speech synthesizer.
|
||||||
* Returns the maximum rate for this speech synthesizer.
|
|
||||||
*/
|
|
||||||
pub fn max_rate(&self) -> f32 {
|
pub fn max_rate(&self) -> f32 {
|
||||||
self.0.max_rate()
|
self.0.read().unwrap().max_rate()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Returns the normal rate for this speech synthesizer.
|
||||||
* Returns the normal rate for this speech synthesizer.
|
|
||||||
*/
|
|
||||||
pub fn normal_rate(&self) -> f32 {
|
pub fn normal_rate(&self) -> f32 {
|
||||||
self.0.normal_rate()
|
self.0.read().unwrap().normal_rate()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Gets the current speech rate.
|
||||||
* Gets the current speech rate.
|
|
||||||
*/
|
|
||||||
pub fn get_rate(&self) -> Result<f32, Error> {
|
pub fn get_rate(&self) -> Result<f32, Error> {
|
||||||
let Features { rate, .. } = self.supported_features();
|
let Features { rate, .. } = self.supported_features();
|
||||||
if rate {
|
if rate {
|
||||||
self.0.get_rate()
|
self.0.read().unwrap().get_rate()
|
||||||
} else {
|
} else {
|
||||||
Err(Error::UnsupportedFeature)
|
Err(Error::UnsupportedFeature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Sets the desired speech rate.
|
||||||
* Sets the desired speech rate.
|
|
||||||
*/
|
|
||||||
pub fn set_rate(&mut self, rate: f32) -> Result<&Self, Error> {
|
pub fn set_rate(&mut self, rate: f32) -> Result<&Self, Error> {
|
||||||
let Features {
|
let Features {
|
||||||
rate: rate_feature, ..
|
rate: rate_feature, ..
|
||||||
} = self.supported_features();
|
} = self.supported_features();
|
||||||
if rate_feature {
|
if rate_feature {
|
||||||
if rate < self.0.min_rate() || rate > self.0.max_rate() {
|
let mut backend = self.0.write().unwrap();
|
||||||
|
if rate < backend.min_rate() || rate > backend.max_rate() {
|
||||||
Err(Error::OutOfRange)
|
Err(Error::OutOfRange)
|
||||||
} else {
|
} else {
|
||||||
self.0.set_rate(rate)?;
|
backend.set_rate(rate)?;
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -242,52 +429,43 @@ impl TTS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Returns the minimum pitch for this speech synthesizer.
|
||||||
* Returns the minimum pitch for this speech synthesizer.
|
|
||||||
*/
|
|
||||||
pub fn min_pitch(&self) -> f32 {
|
pub fn min_pitch(&self) -> f32 {
|
||||||
self.0.min_pitch()
|
self.0.read().unwrap().min_pitch()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Returns the maximum pitch for this speech synthesizer.
|
||||||
* Returns the maximum pitch for this speech synthesizer.
|
|
||||||
*/
|
|
||||||
pub fn max_pitch(&self) -> f32 {
|
pub fn max_pitch(&self) -> f32 {
|
||||||
self.0.max_pitch()
|
self.0.read().unwrap().max_pitch()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Returns the normal pitch for this speech synthesizer.
|
||||||
* Returns the normal pitch for this speech synthesizer.
|
|
||||||
*/
|
|
||||||
pub fn normal_pitch(&self) -> f32 {
|
pub fn normal_pitch(&self) -> f32 {
|
||||||
self.0.normal_pitch()
|
self.0.read().unwrap().normal_pitch()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Gets the current speech pitch.
|
||||||
* Gets the current speech pitch.
|
|
||||||
*/
|
|
||||||
pub fn get_pitch(&self) -> Result<f32, Error> {
|
pub fn get_pitch(&self) -> Result<f32, Error> {
|
||||||
let Features { pitch, .. } = self.supported_features();
|
let Features { pitch, .. } = self.supported_features();
|
||||||
if pitch {
|
if pitch {
|
||||||
self.0.get_pitch()
|
self.0.read().unwrap().get_pitch()
|
||||||
} else {
|
} else {
|
||||||
Err(Error::UnsupportedFeature)
|
Err(Error::UnsupportedFeature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Sets the desired speech pitch.
|
||||||
* Sets the desired speech pitch.
|
|
||||||
*/
|
|
||||||
pub fn set_pitch(&mut self, pitch: f32) -> Result<&Self, Error> {
|
pub fn set_pitch(&mut self, pitch: f32) -> Result<&Self, Error> {
|
||||||
let Features {
|
let Features {
|
||||||
pitch: pitch_feature,
|
pitch: pitch_feature,
|
||||||
..
|
..
|
||||||
} = self.supported_features();
|
} = self.supported_features();
|
||||||
if pitch_feature {
|
if pitch_feature {
|
||||||
if pitch < self.0.min_pitch() || pitch > self.0.max_pitch() {
|
let mut backend = self.0.write().unwrap();
|
||||||
|
if pitch < backend.min_pitch() || pitch > backend.max_pitch() {
|
||||||
Err(Error::OutOfRange)
|
Err(Error::OutOfRange)
|
||||||
} else {
|
} else {
|
||||||
self.0.set_pitch(pitch)?;
|
backend.set_pitch(pitch)?;
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -295,52 +473,43 @@ impl TTS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Returns the minimum volume for this speech synthesizer.
|
||||||
* Returns the minimum volume for this speech synthesizer.
|
|
||||||
*/
|
|
||||||
pub fn min_volume(&self) -> f32 {
|
pub fn min_volume(&self) -> f32 {
|
||||||
self.0.min_volume()
|
self.0.read().unwrap().min_volume()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Returns the maximum volume for this speech synthesizer.
|
||||||
* Returns the maximum volume for this speech synthesizer.
|
|
||||||
*/
|
|
||||||
pub fn max_volume(&self) -> f32 {
|
pub fn max_volume(&self) -> f32 {
|
||||||
self.0.max_volume()
|
self.0.read().unwrap().max_volume()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Returns the normal volume for this speech synthesizer.
|
||||||
* Returns the normal volume for this speech synthesizer.
|
|
||||||
*/
|
|
||||||
pub fn normal_volume(&self) -> f32 {
|
pub fn normal_volume(&self) -> f32 {
|
||||||
self.0.normal_volume()
|
self.0.read().unwrap().normal_volume()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Gets the current speech volume.
|
||||||
* Gets the current speech volume.
|
|
||||||
*/
|
|
||||||
pub fn get_volume(&self) -> Result<f32, Error> {
|
pub fn get_volume(&self) -> Result<f32, Error> {
|
||||||
let Features { volume, .. } = self.supported_features();
|
let Features { volume, .. } = self.supported_features();
|
||||||
if volume {
|
if volume {
|
||||||
self.0.get_volume()
|
self.0.read().unwrap().get_volume()
|
||||||
} else {
|
} else {
|
||||||
Err(Error::UnsupportedFeature)
|
Err(Error::UnsupportedFeature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Sets the desired speech volume.
|
||||||
* Sets the desired speech volume.
|
|
||||||
*/
|
|
||||||
pub fn set_volume(&mut self, volume: f32) -> Result<&Self, Error> {
|
pub fn set_volume(&mut self, volume: f32) -> Result<&Self, Error> {
|
||||||
let Features {
|
let Features {
|
||||||
volume: volume_feature,
|
volume: volume_feature,
|
||||||
..
|
..
|
||||||
} = self.supported_features();
|
} = self.supported_features();
|
||||||
if volume_feature {
|
if volume_feature {
|
||||||
if volume < self.0.min_volume() || volume > self.0.max_volume() {
|
let mut backend = self.0.write().unwrap();
|
||||||
|
if volume < backend.min_volume() || volume > backend.max_volume() {
|
||||||
Err(Error::OutOfRange)
|
Err(Error::OutOfRange)
|
||||||
} else {
|
} else {
|
||||||
self.0.set_volume(volume)?;
|
backend.set_volume(volume)?;
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -348,15 +517,167 @@ impl TTS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Returns whether this speech synthesizer is speaking.
|
||||||
* Returns whether this speech synthesizer is speaking.
|
|
||||||
*/
|
|
||||||
pub fn is_speaking(&self) -> Result<bool, Error> {
|
pub fn is_speaking(&self) -> Result<bool, Error> {
|
||||||
let Features { is_speaking, .. } = self.supported_features();
|
let Features { is_speaking, .. } = self.supported_features();
|
||||||
if is_speaking {
|
if is_speaking {
|
||||||
self.0.is_speaking()
|
self.0.read().unwrap().is_speaking()
|
||||||
} else {
|
} else {
|
||||||
Err(Error::UnsupportedFeature)
|
Err(Error::UnsupportedFeature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns list of available voices.
|
||||||
|
pub fn voices(&self) -> Result<Vec<Voice>, Error> {
|
||||||
|
let Features { voice, .. } = self.supported_features();
|
||||||
|
if voice {
|
||||||
|
self.0.read().unwrap().voices()
|
||||||
|
} else {
|
||||||
|
Err(Error::UnsupportedFeature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the current speaking voice.
|
||||||
|
pub fn voice(&self) -> Result<Option<Voice>, Error> {
|
||||||
|
let Features { get_voice, .. } = self.supported_features();
|
||||||
|
if get_voice {
|
||||||
|
self.0.read().unwrap().voice()
|
||||||
|
} else {
|
||||||
|
Err(Error::UnsupportedFeature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set speaking voice.
|
||||||
|
pub fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> {
|
||||||
|
let Features {
|
||||||
|
voice: voice_feature,
|
||||||
|
..
|
||||||
|
} = self.supported_features();
|
||||||
|
if voice_feature {
|
||||||
|
self.0.write().unwrap().set_voice(voice)
|
||||||
|
} else {
|
||||||
|
Err(Error::UnsupportedFeature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when this speech synthesizer begins speaking an utterance.
|
||||||
|
pub fn on_utterance_begin(
|
||||||
|
&self,
|
||||||
|
callback: Option<Box<dyn FnMut(UtteranceId)>>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let Features {
|
||||||
|
utterance_callbacks,
|
||||||
|
..
|
||||||
|
} = self.supported_features();
|
||||||
|
if utterance_callbacks {
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let id = self.0.read().unwrap().id().unwrap();
|
||||||
|
let callbacks = callbacks.get_mut(&id).unwrap();
|
||||||
|
callbacks.utterance_begin = callback;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::UnsupportedFeature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when this speech synthesizer finishes speaking an utterance.
|
||||||
|
pub fn on_utterance_end(
|
||||||
|
&self,
|
||||||
|
callback: Option<Box<dyn FnMut(UtteranceId)>>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let Features {
|
||||||
|
utterance_callbacks,
|
||||||
|
..
|
||||||
|
} = self.supported_features();
|
||||||
|
if utterance_callbacks {
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let id = self.0.read().unwrap().id().unwrap();
|
||||||
|
let callbacks = callbacks.get_mut(&id).unwrap();
|
||||||
|
callbacks.utterance_end = callback;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::UnsupportedFeature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when this speech synthesizer is stopped and still has utterances in its queue.
|
||||||
|
pub fn on_utterance_stop(
|
||||||
|
&self,
|
||||||
|
callback: Option<Box<dyn FnMut(UtteranceId)>>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let Features {
|
||||||
|
utterance_callbacks,
|
||||||
|
..
|
||||||
|
} = self.supported_features();
|
||||||
|
if utterance_callbacks {
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
let id = self.0.read().unwrap().id().unwrap();
|
||||||
|
let callbacks = callbacks.get_mut(&id).unwrap();
|
||||||
|
callbacks.utterance_stop = callback;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::UnsupportedFeature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns `true` if a screen reader is available to provide speech.
|
||||||
|
*/
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
pub fn screen_reader_available() -> bool {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
#[cfg(feature = "tolk")]
|
||||||
|
{
|
||||||
|
let tolk = Tolk::new();
|
||||||
|
return tolk.detect_screen_reader().is_some();
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "tolk"))]
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Tts {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if Rc::strong_count(&self.0) <= 1 {
|
||||||
|
if let Some(id) = self.0.read().unwrap().id() {
|
||||||
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
callbacks.remove(&id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum Gender {
|
||||||
|
Male,
|
||||||
|
Female,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct Voice {
|
||||||
|
pub(crate) id: String,
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) gender: Option<Gender>,
|
||||||
|
pub(crate) language: LanguageTag<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Voice {
|
||||||
|
pub fn id(&self) -> String {
|
||||||
|
self.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> String {
|
||||||
|
self.name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gender(&self) -> Option<Gender> {
|
||||||
|
self.gender
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn language(&self) -> LanguageTag<String> {
|
||||||
|
self.language.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "tts_winrt_bindings"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
|
|
||||||
description = "Internal crate used by `tts`"
|
|
||||||
license = "MIT"
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
winrt = "0.7"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
winrt = "0.7"
|
|
|
@ -1,12 +0,0 @@
|
||||||
winrt::build!(
|
|
||||||
dependencies
|
|
||||||
os
|
|
||||||
types
|
|
||||||
windows::media::core::MediaSource
|
|
||||||
windows::media::playback::{MediaPlaybackItem, MediaPlaybackList, MediaPlaybackState, MediaPlayer}
|
|
||||||
windows::media::speech_synthesis::SpeechSynthesizer
|
|
||||||
);
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
build();
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
include!(concat!(env!("OUT_DIR"), "/winrt.rs"));
|
|
Loading…
Reference in New Issue
Block a user