From e6bae8806bbb0ed7fe1420b0b951320008d5a5f7 Mon Sep 17 00:00:00 2001 From: Liam Dyer Date: Wed, 16 Aug 2023 23:30:14 -0400 Subject: [PATCH] feat: simplified build based on rustav --- .github/workflows/build.yaml | 38 + .github/workflows/ci.yml | 55 - .github/workflows/publish.yaml | 61 +- .gitignore | 200 +- .gitmodules | 3 - .npmignore | 5 - .npmrc | 1 - Cargo.lock | 278 +- Cargo.toml | 12 +- Dockerfile | 103 +- README.md | 13 +- benches/phonetics.rs | 2 +- libs/bindings/Cargo.toml | 23 + libs/bindings/LICENCE | 21 + libs/bindings/README.md | 5 + libs/bindings/rustfmt.toml | 20 + libs/bindings/src/error.rs | 109 + libs/bindings/src/lib.rs | 388 ++ libs/bindings/src/structs.rs | 84 + libs/bindings/src/utils.rs | 53 + libs/bindings/test_data/hello_world.pho | 1 + .../bindings/test_data/hello_world_mbrola.pho | 10 + libs/bindings/tests/base.rs | 3 + libs/bindings/tests/binding_test.rs | 61 + libs/bindings/tests/parameter.rs | 15 + libs/bindings/tests/phoneme.rs | 30 + libs/bindings/tests/voices.rs | 12 + libs/sys/COPYRIGHT.md | 1 + libs/sys/Cargo.toml | 19 + libs/sys/LICENSE-APACHE | 14 + libs/sys/LICENSE-MIT | 8 + libs/sys/README.md | 169 + libs/sys/build.rs | 98 + libs/sys/headers/encoding.h | 104 + libs/sys/headers/espeak_ng.h | 210 + libs/sys/headers/speak_lib.h | 709 +++ libs/sys/headers/wrapper.h | 3 + libs/sys/src/lib.rs | 5 + npm/linux-x64-gnu/README.md | 3 - npm/linux-x64-gnu/package.json | 28 - package-lock.json | 46 +- package.json | 15 +- tests/assets/frankenstein.txt | 3826 ----------------- tests/common.rs | 35 - tests/test.rs | 106 - 45 files changed, 2532 insertions(+), 4473 deletions(-) create mode 100644 .github/workflows/build.yaml delete mode 100644 .github/workflows/ci.yml delete mode 100644 .gitmodules delete mode 100644 .npmrc create mode 100644 libs/bindings/Cargo.toml create mode 100644 libs/bindings/LICENCE create mode 100644 libs/bindings/README.md create mode 100644 libs/bindings/rustfmt.toml create mode 100644 libs/bindings/src/error.rs create mode 100644 libs/bindings/src/lib.rs create mode 100644 libs/bindings/src/structs.rs create mode 100644 libs/bindings/src/utils.rs create mode 100644 libs/bindings/test_data/hello_world.pho create mode 100644 libs/bindings/test_data/hello_world_mbrola.pho create mode 100644 libs/bindings/tests/base.rs create mode 100644 libs/bindings/tests/binding_test.rs create mode 100644 libs/bindings/tests/parameter.rs create mode 100644 libs/bindings/tests/phoneme.rs create mode 100644 libs/bindings/tests/voices.rs create mode 100644 libs/sys/COPYRIGHT.md create mode 100644 libs/sys/Cargo.toml create mode 100644 libs/sys/LICENSE-APACHE create mode 100644 libs/sys/LICENSE-MIT create mode 100644 libs/sys/README.md create mode 100644 libs/sys/build.rs create mode 100644 libs/sys/headers/encoding.h create mode 100644 libs/sys/headers/espeak_ng.h create mode 100644 libs/sys/headers/speak_lib.h create mode 100644 libs/sys/headers/wrapper.h create mode 100644 libs/sys/src/lib.rs delete mode 100644 npm/linux-x64-gnu/README.md delete mode 100644 npm/linux-x64-gnu/package.json delete mode 100644 tests/assets/frankenstein.txt delete mode 100644 tests/common.rs delete mode 100644 tests/test.rs diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..0708854 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,38 @@ +name: Build +on: + push: + branches: + - main + pull_request: + types: [opened, reopened, synchronize] + +jobs: + build: + runs-on: + labels: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v3 + - uses: depot/setup-action@v1 + + - uses: depot/build-push-action@v1 + with: + context: . + project: hht03j11d7 + outputs: type=local,dest=. + target: binaries + platforms: linux/amd64,linux/arm64 + + - name: Flatten files + run: mv linux_arm64/* . && mv linux_amd64/* . + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: binaries + path: | + *.node + index.js + index.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index acb4186..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Build -on: - push: - branches: - - main - pull_request: - types: [opened, reopened, synchronize] - -jobs: - build: - runs-on: - labels: ubuntu-latest - strategy: - matrix: - platform: [ amd64, arm64 ] - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v3 - - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - with: - platforms: linux/${{ matrix.platform }} - - - - name: Cache Docker layers - uses: actions/cache@v3 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ matrix.platform }}-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx-${{ matrix.platform }}- - - - name: Build - uses: docker/build-push-action@v4 - with: - context: . - outputs: type=local,dest=. - target: binaries - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache - platforms: linux/${{ matrix.platform }} - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: binaries-${{ matrix.platform }} - path: | - rustav.*.node - index.js - index.d.ts - diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 0cf6b16..5a29d3f 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -36,12 +36,10 @@ jobs: runs-on: labels: ubuntu-latest permissions: - contents: read + contents: write + id-token: write packages: write - needs: [ bump-version ] - strategy: - matrix: - platform: [ amd64, arm64 ] + needs: [bump-version] steps: - name: Checkout uses: actions/checkout@v3 @@ -56,54 +54,17 @@ jobs: - name: Set package.json version run: npm version ${{ needs.bump-version.outputs.version }} --no-git-tag-version - - name: Setup QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - with: - platforms: linux/${{ matrix.platform }} - - - name: Cache Docker layers - uses: actions/cache@v3 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ matrix.platform }}-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx-${{ matrix.platform }}- - - - name: Build - uses: docker/build-push-action@v4 + - uses: depot/setup-action@v1 + - uses: depot/build-push-action@v1 with: context: . + project: hht03j11d7 outputs: type=local,dest=. target: binaries - platforms: linux/${{ matrix.platform }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache - - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: binaries-${{ matrix.platform }} - path: | - rustav.*.node - release: - name: Release - runs-on: - labels: ubuntu-latest - permissions: - contents: write - packages: write - needs: - - build - - bump-version - steps: - - name: Checkout - uses: actions/checkout@v3 + platforms: linux/amd64,linux/arm64 - - uses: actions/download-artifact@v3 + - name: Flatten files + run: mv linux_arm64/* . && mv linux_amd64/* . - name: Release uses: softprops/action-gh-release@v1 @@ -111,7 +72,9 @@ jobs: generate_release_notes: true tag_name: ${{ needs.bump-version.outputs.version }} files: | - **/rustav.*.node + *.node + index.d.ts + index.js - name: Authenticate to GitHub Packages run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" > .npmrc diff --git a/.gitignore b/.gitignore index 99d6b97..fe393f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,197 @@ -.cargo -node_modules -target +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# End of https://www.toptal.com/developers/gitignore/api/node + +# Created by https://www.toptal.com/developers/gitignore/api/macos +# Edit at https://www.toptal.com/developers/gitignore?templates=macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +# End of https://www.toptal.com/developers/gitignore/api/macos + +# Created by https://www.toptal.com/developers/gitignore/api/windows +# Edit at https://www.toptal.com/developers/gitignore?templates=windows + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/windows + +#Added by cargo + +/target +Cargo.lock + +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + *.node -.vscode \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 64860a9..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "espeakng-rs"] - path = espeakng-rs - url = https://github.com/GnomedDev/espeakNG-rs diff --git a/.npmignore b/.npmignore index f96abe0..a8f44bf 100644 --- a/.npmignore +++ b/.npmignore @@ -2,9 +2,4 @@ target Cargo.lock .cargo .github -npm -.eslintrc -.prettierignore rustfmt.toml -yarn.lock -*.node diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 5e49306..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -@speechifyinc:registry=https://npm.pkg.github.com diff --git a/Cargo.lock b/Cargo.lock index 0c95b58..8c80173 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" dependencies = [ "memchr", ] @@ -93,9 +93,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "bumpalo" @@ -111,9 +111,12 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "libc", +] [[package]] name = "cexpr" @@ -210,7 +213,7 @@ dependencies = [ "clap", "criterion-plot", "futures", - "itertools 0.10.5", + "itertools", "lazy_static", "num-traits", "oorandom", @@ -232,7 +235,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", - "itertools 0.10.5", + "itertools", ] [[package]] @@ -288,12 +291,6 @@ dependencies = [ "syn 2.0.28", ] -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - [[package]] name = "either" version = "1.9.0" @@ -333,18 +330,16 @@ dependencies = [ ] [[package]] -name = "espeak-rs" +name = "espeak-ng-rs" version = "0.0.0" dependencies = [ "criterion", "espeakng", "levenshtein", - "markov", "napi", "napi-build", "napi-derive", "once_cell", - "pretty_assertions", "regex", "tokio", ] @@ -352,7 +347,6 @@ dependencies = [ [[package]] name = "espeakng" version = "0.1.1" -source = "git+https://github.com/SpeechifyInc/espeakNG-rs#972fe6eafa442972f9b1f688fbf5fe1b1bc53705" dependencies = [ "cfg-if", "errno 0.2.8", @@ -368,7 +362,6 @@ dependencies = [ [[package]] name = "espeakng-sys" version = "0.1.2" -source = "git+https://github.com/SpeechifyInc/espeakng-sys?branch=static-linking#a55eb4598c2b9d3d91dccbc074345e763cd8896e" dependencies = [ "bindgen", ] @@ -379,12 +372,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" -[[package]] -name = "fixedbitset" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" - [[package]] name = "futures" version = "0.3.28" @@ -446,26 +433,6 @@ dependencies = [ "pin-utils", ] -[[package]] -name = "getopts" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "gimli" version = "0.27.3" @@ -524,15 +491,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "itertools" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.10.5" @@ -591,17 +549,11 @@ dependencies = [ "winapi", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "lock_api" @@ -615,24 +567,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" - -[[package]] -name = "markov" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6ad68e26d51a9558f65e93b9795c9422630d0932717a3235668bb9ab71e3fd" -dependencies = [ - "getopts", - "itertools 0.9.0", - "petgraph", - "rand", - "serde", - "serde_derive", - "serde_yaml", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" @@ -666,11 +603,11 @@ dependencies = [ [[package]] name = "napi" -version = "2.13.2" +version = "2.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ede2d12cd6fce44da537a4be1f5510c73be2506c2e32dfaaafd1f36968f3a0e" +checksum = "fd063c93b900149304e3ba96ce5bf210cd4f81ef5eb80ded0d100df3e85a3ac0" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "ctor", "napi-derive", "napi-sys", @@ -807,21 +744,11 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" -[[package]] -name = "petgraph" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" -dependencies = [ - "fixedbitset", - "indexmap", -] - [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -857,22 +784,6 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "pretty_assertions" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" -dependencies = [ - "diff", - "yansi", -] - [[package]] name = "prettyplease" version = "0.2.12" @@ -901,47 +812,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom", - "libc", - "rand_chacha", - "rand_core", - "rand_hc", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core", -] - [[package]] name = "rayon" version = "1.7.0" @@ -975,9 +845,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ "aho-corasick", "memchr", @@ -987,9 +857,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ "aho-corasick", "memchr", @@ -1016,11 +886,11 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno 0.3.2", "libc", "linux-raw-sys", @@ -1062,18 +932,18 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.180" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.180" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", @@ -1082,27 +952,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", "serde", ] -[[package]] -name = "serde_yaml" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" -dependencies = [ - "indexmap", - "ryu", - "serde", - "yaml-rust", -] - [[package]] name = "shlex" version = "1.1.0" @@ -1152,9 +1010,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" dependencies = [ "cfg-if", "fastrand", @@ -1181,11 +1039,10 @@ dependencies = [ [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "num_cpus", "pin-project-lite", @@ -1215,12 +1072,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - [[package]] name = "walkdir" version = "2.3.3" @@ -1231,12 +1082,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasm-bindgen" version = "0.2.87" @@ -1343,9 +1188,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "d1eeca1c172a285ee6c2c84c341ccea837e7c01b12fbb2d0fe3c9e550ce49ec8" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -1358,60 +1203,45 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "b10d0c968ba7f6166195e13d593af609ec2e3d24f916f081690695cf5eaffb2f" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "571d8d4e62f26d4932099a9efe89660e8bd5087775a2ab5cdd8b747b811f1058" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "2229ad223e178db5fbbc8bd8d3835e51e566b8474bfca58d2e6150c48bb723cd" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "600956e2d840c194eedfc5d18f8242bc2e17c7775b6684488af3a9fff6fe3287" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "ea99ff3f8b49fb7a8e0d305e5aec485bd068c2ba691b6e277d29eaeac945868a" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "8f1a05a1ece9a7a0d5a7ccf30ba2c33e3a61a30e042ffd247567d1de1d94120d" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "yansi" -version = "0.5.1" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +checksum = "d419259aba16b663966e29e6d7c6ecfa0bb8425818bb96f6f1f3c3eb71a6e7b9" [[package]] name = "zstr" diff --git a/Cargo.toml b/Cargo.toml index 3334b3c..1185909 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,18 @@ [package] edition = "2021" -name = "espeak-rs" +name = "espeak-ng-rs" version = "0.0.0" [lib] crate-type = ["cdylib", "lib"] -name = "espeak_rs" +name = "espeak_ng_rs" path = "src/lib.rs" [dependencies] # https://nodejs.org/api/n-api.html#node-api-version-matrix -napi = { version = "2.2.0", default-features = false, features = ["napi8", "async", "tokio_rt"] } -napi-derive = "2.2.0" -espeakng = { git = "https://github.com/SpeechifyInc/espeakNG-rs", features = ["static"] } +napi = { version = "2.12.2", default-features = false, features = ["napi8", "async", "tokio_rt"] } +napi-derive = "2.12.2" +espeakng = { path = "./libs/bindings" } regex = "1" levenshtein = "1.0.5" once_cell = "1.17.1" @@ -21,8 +21,6 @@ once_cell = "1.17.1" napi-build = "2.0.1" [dev-dependencies] -markov = "1.1.0" -pretty_assertions = "1.2.1" criterion = { version = "0.4.0", features = ["async_tokio", "html_reports"] } tokio = { version = "1", features = ["rt", "macros"] } diff --git a/Dockerfile b/Dockerfile index c609e63..936f502 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,92 +1,23 @@ -FROM ubuntu:23.04 as builder +FROM rust:1-bookworm as builder -# Set the DEBIAN_FRONTEND environment variable to 'noninteractive' to avoid prompts -# In particular, this prevents tzdata from asking for the timezone -ENV DEBIAN_FRONTEND=noninteractive +WORKDIR /usr/src/espeak-rs -RUN apt-get update && \ - apt-get -y install gcc \ - libclang-dev \ - npm \ - autotools-dev \ - automake \ - curl \ - fd-find \ - build-essential \ - libc6-dev \ - libstdc++-10-dev \ - wget \ - git \ - libfftw3-dev \ - autoconf \ - libtool \ - pkgconf && \ - apt-get clean - -WORKDIR /app - -# Install Rust. Use nightly (faster build times) -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain nightly - -# install sonic -RUN git clone https://github.com/waywardgeek/sonic && \ - cd sonic && \ - make && \ - make install && \ - cd .. && \ - rm -rf sonic - -# Install pcaudiolib -RUN git clone https://github.com/espeak-ng/pcaudiolib && \ - cd pcaudiolib && \ - ./autogen.sh && \ - ./configure CFLAGS="-O3 -march=native -flto -fPIC" --enable-static --disable-shared && \ - make && \ - make install && \ - cd .. && \ - rm -rf pcaudiolib - -# Install alsa -RUN git clone https://github.com/alsa-project/alsa-lib && \ - cd alsa-lib && \ - libtoolize --force --copy --automake && \ - aclocal && \ - autoheader && \ - automake --force-missing --add-missing && \ - autoconf && \ - ./configure CFLAGS="-O3 -march=native -flto -fPIC" --enable-shared=no --enable-static=yes && \ - make && \ - make install && \ - cd .. && \ - rm -rf alsa-lib - -# Install espeak-ng -RUN git clone https://github.com/espeak-ng/espeak-ng && \ - cd espeak-ng && \ - ./autogen.sh && \ - ./configure CFLAGS="-O3 -march=native -flto -fPIC" --enable-static --disable-shared && \ - make && \ - make install && \ - cd .. && \ - rm -rf espeak-ng - -# Add Rust to the PATH -ENV PATH="/root/.cargo/bin:${PATH}" - -RUN npm install -g @napi-rs/cli +RUN apt-get update && apt-get install -y libespeak-ng-dev libclang-dev nodejs +# Install Node +RUN curl -sL https://deb.nodesource.com/setup_18.x | bash && apt-get install -y nodejs +COPY package*.json . +RUN --mount=type=cache,target=/usr/src/espeak-rs/node_modules \ + --mount=type=cache,target=~/.npm \ + npm ci COPY . . - -RUN cargo test && \ - cargo clean - -RUN npm ci && \ - npm install && \ - npm run build && \ - strip *.node && \ - rm -rf node_modules - +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/src/espeak-rs/target \ + --mount=type=cache,target=/usr/src/espeak-rs/node_modules \ + --mount=type=cache,target=~/.npm \ + npm run build FROM scratch as binaries - -COPY --from=builder /app/*.node ./ \ No newline at end of file +COPY --from=builder /usr/src/espeak-rs/*.node . +COPY --from=builder /usr/src/espeak-rs/index.js . +COPY --from=builder /usr/src/espeak-rs/index.d.ts . diff --git a/README.md b/README.md index 33914c5..71aac9b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# espeak-rs -Rust bindings for espeak-ng exposed to Node via N-API. The projects serves to provide improved phonemization performance by directly interacting with espeak instead of spawning a process for each phonemization request. Performance on a 3700x shows about 1ms/50 chars/thread. +# espeak-ng-rs +Rust bindings for espeak-ng exposed to Node via N-API. The projects serves to provide improved phonemization performance by directly interacting with espeak instead of spawning a process for each phonemization request. Performance on a 3700x shows about 1ms/50 chars/thread. ## Usage ```ts -import { EspeakAddon } from 'espeak-rs' +import { EspeakAddon } from 'espeak-ng-rs' const espeak = EspeakAddon.default() const phonemes = espeak.textToPhonemes('Hello world') @@ -12,4 +12,9 @@ const phonemes = espeak.textToPhonemes('Hello world') ## Building -`docker-compose up` +`docker build . --output=. --target=binaries` +or +``` +npm i +npm run build +``` diff --git a/benches/phonetics.rs b/benches/phonetics.rs index 4eb2549..be789c4 100644 --- a/benches/phonetics.rs +++ b/benches/phonetics.rs @@ -1,7 +1,7 @@ use criterion::BenchmarkId; use criterion::Criterion; use criterion::{criterion_group, criterion_main}; -use espeak_rs::phonetics::punctuation::extract_punctuation; +use espeak_ng_rs::phonetics::punctuation::extract_punctuation; // This is a struct that tells Criterion.rs to use the "futures" crate's current-thread executor fn criterion_benchmark(c: &mut Criterion) { let input_text = "This, is a piece of text, that has punctuations."; diff --git a/libs/bindings/Cargo.toml b/libs/bindings/Cargo.toml new file mode 100644 index 0000000..4f855c3 --- /dev/null +++ b/libs/bindings/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "espeakng" +version = "0.1.1" +edition = "2021" +description = "A safe Rust wrapper around espeakNG via espeakNG-sys." +license = "MIT" +repository = "https://github.com/GnomedDev/espeakNG-rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +zstr = "0.1" +espeakng-sys = { path = "../sys" } +libc = "0.2" +errno = "0.2" +strum_macros = "0.23" +once_cell = "1" +cfg-if = "1" +parking_lot = "0.12" +tempfile = "3.5.0" + +[features] +static = ["espeakng-sys/static"] diff --git a/libs/bindings/LICENCE b/libs/bindings/LICENCE new file mode 100644 index 0000000..2f42cc0 --- /dev/null +++ b/libs/bindings/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 David Thomas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/bindings/README.md b/libs/bindings/README.md new file mode 100644 index 0000000..3cb7fea --- /dev/null +++ b/libs/bindings/README.md @@ -0,0 +1,5 @@ +# espeakNG-rs + +A safe Rust wrapper around [espeak NG](https://github.com/espeak-ng/espeak-ng) via [espeakNG-sys](https://github.com/Better-Player/espeakng-sys). + +Once installed, read the documentation with `cargo doc --open -p espeakng`. diff --git a/libs/bindings/rustfmt.toml b/libs/bindings/rustfmt.toml new file mode 100644 index 0000000..9f4653c --- /dev/null +++ b/libs/bindings/rustfmt.toml @@ -0,0 +1,20 @@ +combine_control_expr = true +comment_width = 100 # https://lkml.org/lkml/2020/5/29/1038 +condense_wildcard_suffixes = true +control_brace_style = "AlwaysSameLine" +edition = "2021" +format_code_in_doc_comments = true +format_macro_bodies = true +format_macro_matchers = true +format_strings = true +group_imports = "StdExternalCrate" +imports_granularity = "Crate" +merge_derives = false +newline_style = "Unix" +normalize_comments = true +normalize_doc_attributes = true +overflow_delimited_expr = true +reorder_impl_items = true +reorder_imports = true +unstable_features = true +wrap_comments = true \ No newline at end of file diff --git a/libs/bindings/src/error.rs b/libs/bindings/src/error.rs new file mode 100644 index 0000000..37fbe7a --- /dev/null +++ b/libs/bindings/src/error.rs @@ -0,0 +1,109 @@ +/// An error from this library. +#[derive(Debug)] +pub enum Error { + /// Occured in an espeakng C function. + ESpeakNg(ESpeakNgError), + /// [crate::initialise] was called when already initialized. + AlreadyInit, + /// [crate::Speaker::text_to_phonemes] was called without an mbrola voice selected. + MbrolaWithoutMbrolaVoice, + /// Occured non-espeakng C function, errno is contained if populated. + OtherC(Option), + /// Occurred in an unknown Rust location, usually a library bug. + Other(Box), +} + +impl std::error::Error for Error {} +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&match self { + Self::MbrolaWithoutMbrolaVoice => { + String::from("eSpeak cannot generate mbrola phonemes without an mbrola voice set!") + } + Self::ESpeakNg(err) => { + format!("Failed to execute an internal espeakNG function: {err:?}") + } + Self::AlreadyInit => { + String::from("espeakng::initialise was called after already having been called!") + } + Self::OtherC(err) => format!("Failed to execute an internal C function: {err:?}"), + Self::Other(err) => format!("An internal error occurred: {err:?}"), + }) + } +} + +impl From for Error { + fn from(err: ESpeakNgError) -> Self { + Self::ESpeakNg(err) + } +} + +macro_rules! generate_unknown_err { + ($cause:ty) => { + impl From<$cause> for Error { + fn from(err: $cause) -> Self { + Self::Other(Box::new(err)) + } + } + }; +} + +generate_unknown_err!(std::io::Error); +generate_unknown_err!(std::string::FromUtf8Error); + +/// An error from the `espeakNG` C library. +#[derive(Clone, Copy, Debug, PartialEq, Eq, strum_macros::FromRepr)] +#[allow(clippy::module_name_repetitions)] +#[repr(u32)] +#[rustfmt::skip] +pub enum ESpeakNgError { + CompileError = 0x1000_01FF, + VersionMismatch = 0x1000_02FF, + FifoBufferFull = 0x1000_03FF, + NotInitialized = 0x1000_04FF, + AudioError = 0x1000_05FF, + VoiceNotFound = 0x1000_06FF, + MbrolaNotFound = 0x1000_07FF, + MbrolaVoiceNotFound = 0x1000_08FF, + EventBufferFull = 0x1000_09FF, + NotSupported = 0x1000_0AFF, + UnsupportedPhonemeFormat = 0x1000_0BFF, + NoSpectFrames = 0x1000_0CFF, + EmptyPhonemeManifest = 0x1000_0DFF, + SpeechStopped = 0x1000_0EFF, + UnknownPhonemeFeature = 0x1000_0FFF, + UnknownTextEncoding = 0x1000_10FF, +} + +impl std::error::Error for ESpeakNgError {} +impl std::fmt::Display for ESpeakNgError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + const BUFFER_LEN: usize = 512; + let mut buffer: [libc::c_char; BUFFER_LEN] = [0; BUFFER_LEN]; + + // SAFETY: The size of the buffer is from internal to espeakNG + // if this isn't long enough, internal functions to espeakNG break. + let status_code_message = unsafe { + crate::bindings::espeak_ng_GetStatusCodeMessage( + *self as u32, + buffer.as_mut_ptr(), + BUFFER_LEN, + ); + + std::ffi::CStr::from_ptr(buffer.as_ptr()) + }; + + f.write_str(&status_code_message.to_string_lossy()) + } +} + +pub(crate) fn handle_error(ret_code: u32) -> Result<(), Error> { + if ret_code == 0 { + Ok(()) + } else { + Err(match ESpeakNgError::from_repr(ret_code) { + Some(err) => Error::ESpeakNg(err), + None => Error::OtherC(Some(errno::errno())), + }) + } +} diff --git a/libs/bindings/src/lib.rs b/libs/bindings/src/lib.rs new file mode 100644 index 0000000..81e6e27 --- /dev/null +++ b/libs/bindings/src/lib.rs @@ -0,0 +1,388 @@ +//! A safe Rust wrapper around [espeak NG](https://github.com/espeak-ng/espeak-ng) +//! via [espeakNG-sys](https://github.com/Better-Player/espeakng-sys). +//! +//! ## Safety +//! This library wraps the internal C calls in a singleton ([Speaker]) to keep the mutable global +//! state safe. In this future this may be changed to use the asynchronous features of `espeakNG` +//! however I currently don't trust it to be safe without a global lock. +//! +//! The raw bindings are re-exported via the [bindings] module however usage of this is `unsafe` +//! and all safety guarantees of the [Speaker] object are considered broken if used. +//! +//! ## Known Issues +//! - [`Speaker::synthesize`] seems to emit broken WAV audio data, no idea how to fix. +//! +//! ## Examples +//! Generating phonemes from text: +//! ```rust +//! fn main() -> Result<(), espeakng::Error> { +//! // Get a reference to the global Speaker singleton, using default voice path and buffer length. +//! let mut speaker = espeakng::initialise(None)?.lock(); +//! +//! // Generate the phonemes in standard mode. +//! let phonemes = speaker.text_to_phonemes("Hello World", espeakng::PhonemeGenOptions::Standard)?.unwrap(); +//! println!("Phonemes: {}", phonemes); +//! +//! Ok(()) +//! } +//! ``` +#![warn(unsafe_op_in_unsafe_fn)] +#![warn(clippy::pedantic)] +#![allow( + clippy::cast_sign_loss, clippy::cast_possible_wrap, // Simple `as` conversions that will not fail. + clippy::unused_self, // Speaker needs to take self to keep thread safe. + unused_unsafe // Unsafe is unused in zstr +)] + +use std::{ + ffi::CStr, + io::Read, + marker::PhantomData, + os::{ + fd::IntoRawFd, + unix::prelude::{AsRawFd, FromRawFd}, + }, +}; + +use error::handle_error; +pub use error::{ESpeakNgError, Error}; +pub use espeakng_sys as bindings; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +pub use structs::*; +use zstr::zstr; + +use crate::utils::StringFromCPtr; + +mod error; +mod structs; +mod utils; + +pub type Result = std::result::Result; +type AudioBuffer = Mutex>; + +static SPEAKER: OnceCell> = OnceCell::new(); + +/// Initialise the internal espeak-ng library. If already initialised, that [Speaker] is returned. +/// +/// # Errors +/// If any initialisation steps fail, such as initialising `espeakNG` and setting the default voice. +pub fn initialise(voice_path: Option<&str>) -> Result<&'static Mutex> { + SPEAKER.get_or_try_init(|| Speaker::initialise(voice_path).map(Mutex::new)) +} + +/// Gets the currently initialised [Speaker]. If not set, none is returned. +pub fn get() -> Option<&'static Mutex> { + SPEAKER.get() +} + +pub struct Speaker { + _marker: PhantomData>, +} + +impl Speaker { + pub const DEFAULT_VOICE: &'static str = "en-US"; + + fn initialise(voice_path: Option<&str>) -> Result { + // unsafe extern "C" fn synth_callback( + // wav: *mut i16, + // sample_count: i32, + // events: *mut bindings::espeak_EVENT, + // ) -> i32 { + // match std::panic::catch_unwind(|| { + // if wav.is_null() || sample_count == 0 { + // return 0; + // } + + // let mut new_ptr = events; + + // // Loop through this C event until the terminate event, as this contains the + // pointer // to the audio buffer + // let terminate_event = loop { + // let event = unsafe { *new_ptr }; + // if event.type_ != bindings::espeak_EVENT_TYPE_espeakEVENT_LIST_TERMINATED { + // break event; + // } + + // new_ptr = unsafe { new_ptr.add(1) }; + // }; + + // unsafe { + // if let Some(audio_buffer) = + // *(terminate_event.user_data as *const Option<&AudioBuffer>) + // { + // let wav_slice: &[i16] = + // std::slice::from_raw_parts_mut(wav, sample_count as usize); + // audio_buffer.lock().extend(wav_slice); + // } + // } + + // 0 + // }) { + // Ok(ret) => ret, + // Err(err) => { + // eprintln!("Panic during Rust -> C -> Rust callback: {err:?}"); + // std::process::abort() + // } + // } + // } + + let voice_path = voice_path.map(utils::null_term); + unsafe { + // bindings::espeak_SetSynthCallback(Some(synth_callback)); + bindings::espeak_ng_InitializePath( + voice_path.map_or(std::ptr::null(), |path| path.as_ptr()), + ); + + handle_error(bindings::espeak_ng_Initialize(std::ptr::null_mut()))?; + // handle_error(bindings::espeak_ng_InitializeOutput(1, 0, std::ptr::null()))?; + } + + let mut self_ = Self { + _marker: PhantomData, + }; + self_.set_voice_raw(Self::DEFAULT_VOICE)?; + Ok(self_) + } + + /// Fetch and clone the currently set voice. + #[must_use] + pub fn get_current_voice(&self) -> Voice { + unsafe { + std::ptr::NonNull::new(bindings::espeak_GetCurrentVoice()) + .map(|ptr| Voice::from(*ptr.as_ptr())) + .expect("Voice has somehow been unset!") + } + } + + /// Fetch the espeak voices currently installed. + #[must_use] + pub fn get_voices() -> Vec { + let mut array = unsafe { bindings::espeak_ListVoices(std::ptr::null_mut()) }; + let mut buf = Vec::new(); + + unsafe { + loop { + let next = array.read(); + + if next.is_null() { + break buf; + } + + buf.push(Voice::from(*next)); + array = array.add(1); + } + } + } + + /// Set the voice for future espeak calls. + /// + /// # Errors + /// See [`Speaker::set_voice_raw`] + pub fn set_voice(&mut self, voice: &Voice) -> Result<()> { + self.set_voice_raw(&voice.filename) + } + + /// Set the voice for future espeak calls based on the filename + /// + /// # Errors + /// [`ESpeakNgError::VoiceNotFound`] + pub fn set_voice_raw(&mut self, filename: &str) -> Result<()> { + let mbrola_voice = filename.starts_with("mb/"); + + // We have to do our own VoiceNotFound check as espeakNG seems to internally fail at that. + if mbrola_voice { + let mut voice_path = Self::info().1; + voice_path.push(format!("voices/{filename}")); + if !voice_path.exists() { + return Err(Error::ESpeakNg(ESpeakNgError::VoiceNotFound)); + } + } + + let name_null_term = utils::null_term(filename); + if mbrola_voice { + // Now we are sure the voice is set, we can loop until espeakNG shuts up. + while let Err(err) = + handle_error(unsafe { bindings::espeak_ng_SetVoiceByName(name_null_term.as_ptr()) }) + { + if let Error::ESpeakNg(espeak_err) = err { + if espeak_err == ESpeakNgError::VoiceNotFound { + continue; + } + } + + return Err(err); + } + } else { + handle_error(unsafe { bindings::espeak_ng_SetVoiceByName(name_null_term.as_ptr()) })?; + } + + Ok(()) + } + + /// Get the value of either the currently set or default value of a settings parameter. + pub fn get_parameter(&mut self, param: Parameter, default: bool) -> i32 { + unsafe { bindings::espeak_GetParameter(param as u32, i32::from(!default)) } + } + + /// Set a settings parameter for future espeak calls. + /// + /// # Errors + /// - If a value out of range of the parameter is passed. + /// - If the internal C call fails. + pub fn set_parameter( + &mut self, + param: Parameter, + new_value: i32, + relative: bool, + ) -> Result<()> { + handle_error(unsafe { + bindings::espeak_ng_SetParameter(param as u32, new_value, i32::from(relative)) + }) + } + + /// Get the version string and voice path of the internal C library. + #[must_use] + pub fn info() -> (String, std::path::PathBuf) { + let mut c_voice_path: *const libc::c_char = std::ptr::null(); + + unsafe { + let version_string = bindings::espeak_Info(std::ptr::addr_of_mut!(c_voice_path)); + + ( + String::from_cptr(version_string), + std::path::PathBuf::from(String::from_cptr(c_voice_path)), + ) + } + } + + fn _synthesize(&mut self, text: &str, user_data: Option<&AudioBuffer>) -> Result<()> { + let text_nul_term = utils::null_term(text); + + handle_error(unsafe { + bindings::espeak_ng_Synthesize( + text_nul_term.as_ptr().cast::(), + text_nul_term.len(), + 0, + bindings::espeak_POSITION_TYPE_POS_CHARACTER, + 0, + bindings::espeakCHARS_UTF8, + std::ptr::null_mut(), + (&user_data.map(|ud| ud as *const _) as *const _) as *mut std::ffi::c_void, + ) + })?; + + // Wait until TTS has finished being generated, could be made concurrent but global + // state.... + handle_error(unsafe { bindings::espeak_ng_Synchronize() })?; + + Ok(()) + } + + /// Processes the given text into phonemes, depending on which [`PhonemeGenOptions`] are passed. + /// + /// This will only return [None] if [`PhonemeGenOptions::MbrolaFile`] is passed. + /// + /// # Errors + /// If [`PhonemeGenOptions::Mbrola`] or [`PhonemeGenOptions::MbrolaFile`] is passed, internal C + /// calls may fail. + pub fn text_to_phonemes( + &mut self, + text: &str, + option: PhonemeGenOptions, + ) -> Result> { + let file = match option { + PhonemeGenOptions::MbrolaFile(file) => Some(file), + _ => None, + }; + + match option { + PhonemeGenOptions::Standard => Ok(Some(self.text_to_phonemes_standard(text))), + PhonemeGenOptions::Mbrola | PhonemeGenOptions::MbrolaFile(_) => { + self.text_to_phonemes_mbrola(text, file) + } + } + } + + fn text_to_phonemes_standard(&mut self, text: &str) -> String { + let text_nul_term = utils::null_term(text); + + let output = unsafe { + CStr::from_ptr(bindings::espeak_TextToPhonemes( + &mut text_nul_term.as_ptr().cast() as *mut *const std::ffi::c_void, + bindings::espeakCHARS_UTF8 as i32, + 2, + )) + }; + + output.to_string_lossy().to_string() + } + + fn text_to_phonemes_mbrola( + &mut self, + text: &str, + file: Option<&dyn AsRawFd>, + ) -> Result> { + if !self.get_current_voice().filename.starts_with("mb/") { + return Err(Error::MbrolaWithoutMbrolaVoice); + }; + + let raw_file_fd = match file { + Some(file) => file.as_raw_fd(), + None => tempfile::tempfile()?.into_raw_fd(), + }; + + // Generate fake C File from this FD + let raw_file = unsafe { + let raw_file_ptr = bindings::fdopen(raw_file_fd, zstr!("w+").as_ptr()); + std::ptr::NonNull::new(raw_file_ptr) + .ok_or_else(|| Error::OtherC(Some(errno::errno())))? + }; + + // Set the phoneme output to the stream + unsafe { + bindings::espeak_SetPhonemeTrace( + bindings::espeakPHONEMES_MBROLA as i32, + raw_file.as_ptr(), + ); + } + + // Generate TTS, this will populate the phoneme trace + let result = self._synthesize(text, None); + + // Reset the phoneme trace back to stdout, to avoid side effects + unsafe { bindings::espeak_SetPhonemeTrace(0, std::ptr::null_mut()) }; + + if file.is_none() { + let mut file = unsafe { + // Seek to the start of the fake_file, now it has been written to + bindings::fseek(raw_file.as_ptr(), 0, 0); + + // Transfer FD ownership from C to Rust + let dup_fd = libc::dup(raw_file_fd); + bindings::fclose(raw_file.as_ptr()); + + // SAFETY: ^ must have just occured + std::fs::File::from_raw_fd(dup_fd) + }; + + // Now handle possible errors, as we can return without leak. + result?; + + let mut buf = Vec::new(); + file.read_to_end(&mut buf)?; + Ok(Some(String::from_utf8(buf)?)) + } else { + // Data has been written to the file passed in, close the C version of the file. + unsafe { bindings::fclose(raw_file.as_ptr()) }; + // Now handle possible errors, and if successful get rid of any return value. + result.map(|_| None) + } + } +} + +impl Drop for Speaker { + fn drop(&mut self) { + unsafe { bindings::espeak_ng_Terminate() }; + } +} diff --git a/libs/bindings/src/structs.rs b/libs/bindings/src/structs.rs new file mode 100644 index 0000000..04d85c4 --- /dev/null +++ b/libs/bindings/src/structs.rs @@ -0,0 +1,84 @@ +use std::os::unix::prelude::AsRawFd; + +use crate::{bindings, utils, utils::StringFromCPtr}; + +#[derive(Clone, Copy)] +pub enum PhonemeGenOptions<'a> { + /// Generate phonemes using the standard espeak style + Standard, + /// Generate phonemes using the mbrola style + Mbrola, + /// Generate phonemes using the mbrola style and write them in a file + MbrolaFile(&'a dyn AsRawFd), +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone, strum_macros::FromRepr)] +#[repr(u8)] +pub enum Gender { + Male = 1, + Female = 2, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Language { + pub name: String, + pub priority: i8, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] // Keep Voice private constructable to keep set_voice safe. +pub struct Voice { + pub name: String, + pub filename: String, + pub languages: Vec, + pub gender: Option, + pub age: u8, +} + +impl From for Voice { + fn from(voice: bindings::espeak_VOICE) -> Self { + unsafe { + Self { + age: voice.age, + name: String::from_cptr(voice.name), + filename: String::from_cptr(voice.identifier), + gender: Gender::from_repr(voice.gender), + languages: utils::parse_lang_array(voice.languages), + } + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum Parameter { + /// Words per minute. Values must be between 80-450 inclusive. + Rate = 1, + /// Volume of speech. Values should be 0-100 as greater values may produce amplitude + /// compression or distortion. + Volume = 2, + /// Base pitch, default 50. Values must be between 0-100 inclusive. + Pitch = 3, + /// The pitch range, where 0 is monotone and 50 is normal. Values must be between 0-100 + /// inclusive. + Range = 4, + /// The punctuation characters to speak. Value must be [PunctationType]. + Punctuation = 5, + /// How to pronounce capital letters. + /// - 0 = none + /// - 1 = sound icon + /// - 2 = spelling + /// - 3 or higher, by raising pitch. The value is the amount of Hz by which the pitch of each + /// capitalised word is raised. + Capitals = 6, + /// The units of how long to pause between words. At default speed, this is units of of 10mS. + Wordgap = 7, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum PunctationType { + None = 0, + All = 1, + Some = 2, +} diff --git a/libs/bindings/src/utils.rs b/libs/bindings/src/utils.rs new file mode 100644 index 0000000..5bc4f50 --- /dev/null +++ b/libs/bindings/src/utils.rs @@ -0,0 +1,53 @@ +use std::ffi::CStr; + +pub(crate) fn null_term(s: &str) -> Vec { + let mut nul_term_s: Vec = Vec::with_capacity(s.len()); + nul_term_s.extend(s.as_bytes().iter().map(|i| *i as libc::c_char)); + nul_term_s.push(0); + nul_term_s +} + +pub(crate) unsafe fn parse_lang_array(ptr: *const libc::c_char) -> Vec { + let mut languages = Vec::new(); + let mut ptr = ptr; + + loop { + // SAFETY: It probably isn't + let (name, priority) = unsafe { + if *ptr == 0 { + break; + } + + // First byte is priority + let priority = ptr.read(); + ptr = ptr.add(1); + + // Then we have language, as a null term string + let namelen = libc::strlen(ptr); + let name = std::slice::from_raw_parts(ptr.cast::(), namelen); + + // Move the pointer past the name, plus 1 for the next iter + ptr = ptr.add(namelen + 1); + (name, priority) + }; + + #[allow(clippy::unnecessary_cast)] + languages.push(crate::Language { + name: String::from_utf8(Vec::from(name)).unwrap(), + priority: priority as i8, + }); + } + languages +} + +pub(crate) trait StringFromCPtr { + unsafe fn from_cptr(ptr: *const libc::c_char) -> Self; +} + +impl StringFromCPtr for String { + unsafe fn from_cptr(ptr: *const libc::c_char) -> Self { + unsafe { CStr::from_ptr(ptr) } + .to_string_lossy() + .into_owned() + } +} diff --git a/libs/bindings/test_data/hello_world.pho b/libs/bindings/test_data/hello_world.pho new file mode 100644 index 0000000..281e007 --- /dev/null +++ b/libs/bindings/test_data/hello_world.pho @@ -0,0 +1 @@ +h@l'oU w'3:ld \ No newline at end of file diff --git a/libs/bindings/test_data/hello_world_mbrola.pho b/libs/bindings/test_data/hello_world_mbrola.pho new file mode 100644 index 0000000..50ac9ef --- /dev/null +++ b/libs/bindings/test_data/hello_world_mbrola.pho @@ -0,0 +1,10 @@ +h 70 +@ 24 0 94 20 95 40 96 59 97 80 99 100 99 +l 65 +@U 61 0 117 80 109 100 109 +w 65 +3: 96 0 102 80 76 100 76 +5 65 +d 65 +_ 7 +_ 1 diff --git a/libs/bindings/tests/base.rs b/libs/bindings/tests/base.rs new file mode 100644 index 0000000..cc357a5 --- /dev/null +++ b/libs/bindings/tests/base.rs @@ -0,0 +1,3 @@ +pub fn init<'a>() -> parking_lot::MutexGuard<'a, espeakng::Speaker> { + espeakng::initialise(None).unwrap().lock() +} diff --git a/libs/bindings/tests/binding_test.rs b/libs/bindings/tests/binding_test.rs new file mode 100644 index 0000000..5200ed5 --- /dev/null +++ b/libs/bindings/tests/binding_test.rs @@ -0,0 +1,61 @@ +//! Very basic test for the bindings, should not be used as an example! + +use std::ffi::{c_void, CStr}; + +use espeakng::bindings; +use zstr::zstr; + +#[test] +fn binding_check() -> Result<(), Box> { + let mode = zstr!("r+"); + let en = zstr!("mb-en1"); + let hello_world = zstr!("Hello world"); + + unsafe { + bindings::espeak_Initialize( + bindings::espeak_AUDIO_OUTPUT_AUDIO_OUTPUT_RETRIEVAL, + 0, + std::ptr::null(), + 0, + ); + loop { + let r = bindings::espeak_SetVoiceByName(en.as_ptr()); + if r == 0 { + break; + } + } + + let mut buf = vec![0; 205]; + let fake_file = libc::fmemopen(buf.as_mut_ptr() as *mut libc::c_void, 200, mode.as_ptr()); + + bindings::espeak_SetPhonemeTrace( + bindings::espeakPHONEMES_MBROLA as i32, + std::mem::transmute(fake_file), + ); + + bindings::espeak_ng_Synthesize( + hello_world.as_ptr() as *const c_void, + hello_world.to_bytes_with_nul().len(), + 0, + bindings::espeak_POSITION_TYPE_POS_CHARACTER, + 0, + bindings::espeakCHARS_AUTO, + std::ptr::null_mut(), + std::ptr::null_mut(), + ); + + bindings::espeak_Synchronize(); + bindings::espeak_Terminate(); + + libc::fseek(fake_file, 0, libc::SEEK_END); + buf.set_len(libc::ftell(fake_file) as usize); + libc::fseek(fake_file, 0, libc::SEEK_SET); + + assert_eq!( + CStr::from_ptr(buf.as_ptr()).to_str()?, + include_str!("../test_data/hello_world_mbrola.pho") + ); + }; + + Ok(()) +} diff --git a/libs/bindings/tests/parameter.rs b/libs/bindings/tests/parameter.rs new file mode 100644 index 0000000..015910d --- /dev/null +++ b/libs/bindings/tests/parameter.rs @@ -0,0 +1,15 @@ +mod base; +use base::init; + +#[test] +fn set() { + let mut speaker = init(); + speaker + .set_parameter(espeakng::Parameter::Volume, 1, true) + .unwrap(); + + assert_eq!( + speaker.get_parameter(espeakng::Parameter::Volume, true) + 1, + speaker.get_parameter(espeakng::Parameter::Volume, false) + ); +} diff --git a/libs/bindings/tests/phoneme.rs b/libs/bindings/tests/phoneme.rs new file mode 100644 index 0000000..bd630eb --- /dev/null +++ b/libs/bindings/tests/phoneme.rs @@ -0,0 +1,30 @@ +//! Tests for espeakng::Speaker::text_to_phonemes +mod base; +use base::init; + +#[test] +fn espeak() -> Result<(), espeakng::Error> { + assert_eq!( + init() + .text_to_phonemes("Hello world", espeakng::PhonemeGenOptions::Standard)? + .unwrap(), + include_str!("../test_data/hello_world.pho") + ); + + Ok(()) +} + +#[test] +fn mbrola() -> Result<(), espeakng::Error> { + let mut speaker = init(); + speaker.set_voice_raw("mb/mb-en1")?; + + assert_eq!( + speaker + .text_to_phonemes("Hello world", espeakng::PhonemeGenOptions::Mbrola)? + .unwrap(), + include_str!("../test_data/hello_world_mbrola.pho") + ); + + Ok(()) +} diff --git a/libs/bindings/tests/voices.rs b/libs/bindings/tests/voices.rs new file mode 100644 index 0000000..7465ee3 --- /dev/null +++ b/libs/bindings/tests/voices.rs @@ -0,0 +1,12 @@ +mod base; +use base::init; + +#[test] +fn get_voice() -> espeakng::Result<()> { + assert_eq!( + init().get_current_voice().filename, + espeakng::Speaker::DEFAULT_VOICE + ); + + Ok(()) +} diff --git a/libs/sys/COPYRIGHT.md b/libs/sys/COPYRIGHT.md new file mode 100644 index 0000000..01191ef --- /dev/null +++ b/libs/sys/COPYRIGHT.md @@ -0,0 +1 @@ +This project is dual licenced under the [Apache-2.0](LICENSE-APACHE) and the [MIT](LICENSE-MIT) license, at your discretion \ No newline at end of file diff --git a/libs/sys/Cargo.toml b/libs/sys/Cargo.toml new file mode 100644 index 0000000..aa435ec --- /dev/null +++ b/libs/sys/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "espeakng-sys" +version = "0.1.2" +edition = "2018" +authors = ["Tobias de Bruijn "] +description = "Raw FFI bindings to eSpeak NG" +license = "Apache-2.0 OR MIT" +keywords = ["FFI", "espeak-ng", "audio", "tts", "speech"] +categories = ["external-ffi-bindings", "api-bindings"] +repository = "https://github.com/Better-Player/espeakng-sys" +homepage = "https://github.com/Better-Player/espeakng-sys" +readme = "README.md" + +[build-dependencies.bindgen] +version = "0.65.1" +default-features = false + +[features] +static = [] \ No newline at end of file diff --git a/libs/sys/LICENSE-APACHE b/libs/sys/LICENSE-APACHE new file mode 100644 index 0000000..390c36f --- /dev/null +++ b/libs/sys/LICENSE-APACHE @@ -0,0 +1,14 @@ + + Copyright 2021 Tobias de Bruijn + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/libs/sys/LICENSE-MIT b/libs/sys/LICENSE-MIT new file mode 100644 index 0000000..dfd7c28 --- /dev/null +++ b/libs/sys/LICENSE-MIT @@ -0,0 +1,8 @@ +Copyright 2021 Tobias de Bruijn + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/libs/sys/README.md b/libs/sys/README.md new file mode 100644 index 0000000..d7018d5 --- /dev/null +++ b/libs/sys/README.md @@ -0,0 +1,169 @@ +# eSpeak NG bindings for Rust +![Crates.io](https://img.shields.io/crates/v/espeakng-sys?style=flat) +FFI bindings to the C library eSpeak NG for Rust + +Current eSpeak NG version: 1.51 + +## Dependencies +- [eSpeak NG](https://github.com/espeak-ng/espeak-ng/blob/master/docs/building.md) + +## Example +This example shows how you can convert a &str to a Vec +```rs +#![allow(non_upper_case_globals)] + +use espeakng_sys::*; +use std::os::raw::{c_char, c_short, c_int}; +use std::ffi::{c_void, CString}; +use std::cell::Cell; +use lazy_static::lazy_static; +use std::sync::{Mutex, MutexGuard}; + +/// The name of the voice to use +const VOICE_NAME: &str = "English"; +/// The length in mS of sound buffers passed to the SynthCallback function. +const BUFF_LEN: i32 = 500; +/// Options to set for espeak-ng +const OPTIONS: i32 = 0; + +lazy_static! { + /// The complete audio provided by the callback + static ref AUDIO_RETURN: Mutex>> = Mutex::new(Cell::new(Vec::default())); + + /// Audio buffer for use in the callback + static ref AUDIO_BUFFER: Mutex>> = Mutex::new(Cell::new(Vec::default())); +} + +/// Spoken speech +pub struct Spoken { + /// The audio data + pub wav: Vec, + /// The sample rate of the audio + pub sample_rate: i32 +} + +/// Perform Text-To-Speech +pub fn speak(text: &str) -> Spoken { + let output: espeak_AUDIO_OUTPUT = espeak_AUDIO_OUTPUT_AUDIO_OUTPUT_RETRIEVAL; + + AUDIO_RETURN.plock().set(Vec::default()); + AUDIO_BUFFER.plock().set(Vec::default()); + + // The directory which contains the espeak-ng-data directory, or NULL for the default location. + let path: *const c_char = std::ptr::null(); + let voice_name_cstr = CString::new(VOICE_NAME).expect("Failed to convert &str to CString"); + let voice_name = voice_name_cstr.as_ptr(); + + // Returns: sample rate in Hz, or -1 (EE_INTERNAL_ERROR). + let sample_rate = unsafe { espeak_Initialize(output, BUFF_LEN, path, OPTIONS) }; + + unsafe { + espeak_SetVoiceByName(voice_name as *const c_char); + espeak_SetSynthCallback(Some(synth_callback)) + } + + let text_cstr = CString::new(text).expect("Failed to convert &str to CString"); + + let position = 0u32; + let position_type: espeak_POSITION_TYPE = 0; + let end_position = 0u32; + let flags = espeakCHARS_AUTO; + let identifier = std::ptr::null_mut(); + let user_data = std::ptr::null_mut(); + + unsafe { espeak_Synth(text_cstr.as_ptr() as *const c_void, BUFF_LEN as size_t, position, position_type, end_position, flags, identifier, user_data); } + + // Wait for the speaking to complete + match unsafe { espeak_Synchronize() } { + espeak_ERROR_EE_OK => {}, + espeak_ERROR_EE_INTERNAL_ERROR => { + todo!() + } + _ => unreachable!() + } + + let result = AUDIO_RETURN.plock().take(); + + Spoken { + wav: result, + sample_rate + } +} + +/// int SynthCallback(short *wav, int numsamples, espeak_EVENT *events); +/// +/// wav: is the speech sound data which has been produced. +/// NULL indicates that the synthesis has been completed. +/// +/// numsamples: is the number of entries in wav. This number may vary, may be less than +/// the value implied by the buflength parameter given in espeak_Initialize, and may +/// sometimes be zero (which does NOT indicate end of synthesis). +/// +/// events: an array of espeak_EVENT items which indicate word and sentence events, and +/// also the occurance if and