├── .idea ├── .gitignore ├── kotlinc.xml ├── xcode.xml ├── AndroidProjectSystem.xml ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── migrations.xml ├── deploymentTargetSelector.xml ├── runConfigurations.xml └── runConfigurations │ ├── Create_library_docs.xml │ ├── Run_Browser__JS__Compose_Demo.xml │ ├── Run_Browser__WASM__Compose_Demo.xml │ ├── Build_library.xml │ ├── Run_Desktop_Compose_Demo.xml │ ├── Run_Android_Compose_Demo.xml │ └── Run_Android_XML_Demo.xml ├── .gitattributes ├── tts-compose ├── src │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── nl │ │ │ └── marc_apps │ │ │ └── tts │ │ │ └── TextToSpeech.kt │ ├── commonMain │ │ └── kotlin │ │ │ └── nl │ │ │ └── marc_apps │ │ │ └── tts │ │ │ └── TextToSpeech.kt │ ├── appleMain │ │ └── kotlin │ │ │ └── nl │ │ │ └── marc_apps │ │ │ └── tts │ │ │ └── TextToSpeech.kt │ ├── webCommonW3CMain │ │ └── kotlin │ │ │ └── nl │ │ │ └── marc_apps │ │ │ └── tts │ │ │ └── TextToSpeech.kt │ └── jvmMain │ │ └── kotlin │ │ └── nl │ │ └── marc_apps │ │ └── tts │ │ └── TextToSpeech.kt └── build.gradle.kts ├── tts ├── src │ ├── jsMain │ │ └── kotlin │ │ │ ├── js_interop │ │ │ ├── JsAny.kt │ │ │ ├── SpeechSynthesisVoices.kt │ │ │ └── Window.kt │ │ │ └── org │ │ │ └── w3c │ │ │ └── speech │ │ │ ├── WindowExtensions.kt │ │ │ ├── SpeechSynthesisVoice.kt │ │ │ ├── SpeechSynthesisUtterance.kt │ │ │ └── SpeechSynthesis.kt │ ├── webCommonW3CMain │ │ └── kotlin │ │ │ ├── js_interop │ │ │ ├── JsAny.kt │ │ │ ├── SpeechSynthesisVoices.kt │ │ │ └── Window.kt │ │ │ ├── nl │ │ │ └── marc_apps │ │ │ │ └── tts │ │ │ │ ├── Voice.kt │ │ │ │ ├── BrowserVoice.kt │ │ │ │ ├── TextToSpeechFactory.kt │ │ │ │ └── TextToSpeechBrowser.kt │ │ │ └── org │ │ │ └── w3c │ │ │ └── speech │ │ │ ├── SpeechSynthesisVoice.kt │ │ │ ├── SpeechSynthesisUtterance.kt │ │ │ └── SpeechSynthesis.kt │ ├── wasmJsMain │ │ └── kotlin │ │ │ ├── js_interop │ │ │ ├── JsAny.kt │ │ │ ├── SpeechSynthesisVoices.kt │ │ │ └── Window.kt │ │ │ └── org │ │ │ └── w3c │ │ │ └── speech │ │ │ ├── SpeechSynthesisVoice.kt │ │ │ ├── SpeechSynthesisUtterance.kt │ │ │ └── SpeechSynthesis.kt │ ├── appleMain │ │ └── kotlin │ │ │ └── nl │ │ │ └── marc_apps │ │ │ └── tts │ │ │ ├── Voice.kt │ │ │ ├── TextToSpeechFactory.kt │ │ │ ├── utils │ │ │ ├── ErrorPointerUtils.kt │ │ │ └── TtsProgressConverter.kt │ │ │ ├── IOSVoice.kt │ │ │ └── TextToSpeechIOS.kt │ ├── androidMain │ │ ├── kotlin │ │ │ └── nl │ │ │ │ └── marc_apps │ │ │ │ └── tts │ │ │ │ ├── TextToSpeechSystemSettings.kt │ │ │ │ ├── Voice.kt │ │ │ │ ├── utils │ │ │ │ ├── IdUtils.kt │ │ │ │ ├── VoiceAndroidLegacy.kt │ │ │ │ ├── ErrorCodes.kt │ │ │ │ ├── TtsProgressConverter.kt │ │ │ │ └── VoiceAndroidModern.kt │ │ │ │ ├── TextToSpeechFactory.kt │ │ │ │ └── TextToSpeechAndroid.kt │ │ └── AndroidManifest.xml │ ├── jvmMain │ │ └── kotlin │ │ │ └── nl │ │ │ └── marc_apps │ │ │ └── tts │ │ │ ├── Voice.kt │ │ │ ├── utils │ │ │ └── SynthesisScope.kt │ │ │ ├── TextToSpeechFactory.kt │ │ │ └── TextToSpeechDesktop.kt │ ├── commonMain │ │ └── kotlin │ │ │ └── nl │ │ │ └── marc_apps │ │ │ └── tts │ │ │ ├── UtteranceResult.kt │ │ │ ├── Voice.kt │ │ │ ├── TextToSpeechEngine.kt │ │ │ ├── TextToSpeechFactory.kt │ │ │ ├── experimental │ │ │ ├── ExperimentalIosTarget.kt │ │ │ ├── ExperimentalWasmTarget.kt │ │ │ ├── ExperimentalVoiceApi.kt │ │ │ ├── ExperimentalDesktopTarget.kt │ │ │ └── ExperimentalTextToSpeechApi.kt │ │ │ ├── utils │ │ │ ├── ResultHandler.kt │ │ │ └── CallbackHandler.kt │ │ │ ├── errors │ │ │ ├── TextToSpeechInitialisationErrors.kt │ │ │ └── TextToSpeechSynthesisErrors.kt │ │ │ ├── TextToSpeechInstance.kt │ │ │ └── TextToSpeech.kt │ └── iosMain │ │ └── kotlin │ │ └── nl │ │ └── marc_apps │ │ └── tts │ │ └── AudioSession.kt └── build.gradle.kts ├── demo ├── iosApp │ ├── Configuration │ │ └── Config.xcconfig │ ├── iosApp │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── AppIcon-29.png │ │ │ │ ├── AppIcon@2x.png │ │ │ │ ├── AppIcon@3x.png │ │ │ │ ├── AppIcon~ipad.png │ │ │ │ ├── AppIcon-20@2x.png │ │ │ │ ├── AppIcon-20@3x.png │ │ │ │ ├── AppIcon-29@2x.png │ │ │ │ ├── AppIcon-29@3x.png │ │ │ │ ├── AppIcon-40@2x.png │ │ │ │ ├── AppIcon-40@3x.png │ │ │ │ ├── AppIcon-20~ipad.png │ │ │ │ ├── AppIcon-29~ipad.png │ │ │ │ ├── AppIcon-40~ipad.png │ │ │ │ ├── AppIcon-60@2x~car.png │ │ │ │ ├── AppIcon-60@3x~car.png │ │ │ │ ├── AppIcon@2x~ipad.png │ │ │ │ ├── AppIcon-20@2x~ipad.png │ │ │ │ ├── AppIcon-29@2x~ipad.png │ │ │ │ ├── AppIcon-40@2x~ipad.png │ │ │ │ ├── AppIcon-83.5@2x~ipad.png │ │ │ │ ├── AppIcon~ios-marketing.png │ │ │ │ └── Contents.json │ │ │ └── AccentColor.colorset │ │ │ │ └── Contents.json │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ ├── iOSApp.swift │ │ ├── ContentView.swift │ │ └── Info.plist │ └── iosApp.xcodeproj │ │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── compose-multiplatform │ ├── src │ ├── jsMain │ │ ├── resources │ │ │ ├── favicon.ico │ │ │ ├── apple-touch-icon.png │ │ │ ├── style.css │ │ │ └── index.html │ │ └── kotlin │ │ │ └── Main.kt │ ├── wasmJsMain │ │ ├── resources │ │ │ ├── favicon.ico │ │ │ ├── apple-touch-icon.png │ │ │ ├── load.mjs │ │ │ ├── style.css │ │ │ └── index.html │ │ └── kotlin │ │ │ └── Main.wasm.kt │ ├── androidMain │ │ ├── resources │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_background.webp │ │ │ │ ├── ic_launcher_foreground.webp │ │ │ │ └── ic_launcher_monochrome.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_background.webp │ │ │ │ ├── ic_launcher_foreground.webp │ │ │ │ └── ic_launcher_monochrome.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_background.webp │ │ │ │ ├── ic_launcher_foreground.webp │ │ │ │ └── ic_launcher_monochrome.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_background.webp │ │ │ │ ├── ic_launcher_foreground.webp │ │ │ │ └── ic_launcher_monochrome.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_background.webp │ │ │ │ ├── ic_launcher_foreground.webp │ │ │ │ └── ic_launcher_monochrome.webp │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── values-night │ │ │ │ └── colors.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ └── ic_launcher.xml │ │ │ ├── layout │ │ │ │ ├── activity_main_xml.xml │ │ │ │ └── fragment_main_xml.xml │ │ │ └── drawable │ │ │ │ ├── ic_volume_mute.xml │ │ │ │ └── ic_volume_up.xml │ │ ├── kotlin │ │ │ └── nl │ │ │ │ └── marc_apps │ │ │ │ └── tts_demo │ │ │ │ ├── MainXmlActivity.kt │ │ │ │ ├── di │ │ │ │ └── MainModule.kt │ │ │ │ ├── MainApplication.kt │ │ │ │ ├── ColorSchemeUtils.kt │ │ │ │ ├── TtsDemoViewPreview.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ └── ui │ │ │ │ ├── MainXmlViewModel.kt │ │ │ │ └── MainXmlFragment.kt │ │ └── AndroidManifest.xml │ ├── commonMain │ │ ├── kotlin │ │ │ └── nl │ │ │ │ └── marc_apps │ │ │ │ └── tts_demo │ │ │ │ ├── AppTheme.kt │ │ │ │ ├── ColorSchemeUtils.kt │ │ │ │ ├── AppColorScheme.kt │ │ │ │ ├── TtsDemoApp.kt │ │ │ │ ├── TtsDemoView.kt │ │ │ │ └── OptionsCard.kt │ │ └── composeResources │ │ │ ├── values │ │ │ └── strings.xml │ │ │ ├── values-en-rUS │ │ │ └── strings.xml │ │ │ └── values-nl │ │ │ └── strings.xml │ ├── macosMain │ │ └── kotlin │ │ │ ├── Main.kt │ │ │ └── nl │ │ │ └── marc_apps │ │ │ └── tts_demo │ │ │ └── ColorSchemeUtils.kt │ ├── jvmMain │ │ └── kotlin │ │ │ ├── Main.kt │ │ │ └── nl │ │ │ └── marc_apps │ │ │ └── tts_demo │ │ │ ├── ColorSchemeUtils.kt │ │ │ └── TtsDemoViewPreview.kt │ ├── iosMain │ │ └── kotlin │ │ │ └── nl │ │ │ └── marc_apps │ │ │ └── tts_demo │ │ │ ├── ColorSchemeUtils.kt │ │ │ └── MainViewController.kt │ └── webCommonW3CMain │ │ └── kotlin │ │ └── nl │ │ └── marc_apps │ │ └── tts_demo │ │ └── ColorSchemeUtils.kt │ └── build.gradle.kts ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── settings.gradle.kts ├── LICENSE ├── .github └── workflows │ ├── validation.yml │ └── deployment.yml ├── gradlew.bat ├── gradle.properties └── README.md /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /tts-compose/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tts/src/jsMain/kotlin/js_interop/JsAny.kt: -------------------------------------------------------------------------------- 1 | package js_interop 2 | 3 | actual external interface JsAny 4 | -------------------------------------------------------------------------------- /tts/src/webCommonW3CMain/kotlin/js_interop/JsAny.kt: -------------------------------------------------------------------------------- 1 | package js_interop 2 | 3 | expect interface JsAny 4 | -------------------------------------------------------------------------------- /demo/iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=nl.marc_apps.tts_demo.TTSDemo 3 | APP_NAME=TTS Demo -------------------------------------------------------------------------------- /tts/src/wasmJsMain/kotlin/js_interop/JsAny.kt: -------------------------------------------------------------------------------- 1 | package js_interop 2 | 3 | actual typealias JsAny = kotlin.js.JsAny 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/jsMain/resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/jsMain/resources/favicon.ico -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/wasmJsMain/resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/wasmJsMain/resources/favicon.ico -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/jsMain/resources/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/jsMain/resources/apple-touch-icon.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/wasmJsMain/resources/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/wasmJsMain/resources/apple-touch-icon.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/wasmJsMain/resources/load.mjs: -------------------------------------------------------------------------------- 1 | import { instantiate } from './compose-multiplatform.uninstantiated.mjs'; 2 | 3 | await wasmSetup; 4 | 5 | instantiate({ skia: Module['asm'] }); 6 | -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png -------------------------------------------------------------------------------- /tts/src/appleMain/kotlin/nl/marc_apps/tts/Voice.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 4 | 5 | @ExperimentalVoiceApi 6 | actual interface Voice : CommonVoice 7 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /tts/src/jsMain/kotlin/org/w3c/speech/WindowExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.w3c.speech 2 | 3 | import org.w3c.dom.Window 4 | 5 | inline val Window.speechSynthesis: SpeechSynthesis 6 | get() = asDynamic().speechSynthesis 7 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /tts/src/webCommonW3CMain/kotlin/nl/marc_apps/tts/Voice.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 4 | 5 | @ExperimentalVoiceApi 6 | actual interface Voice : CommonVoice 7 | -------------------------------------------------------------------------------- /.idea/xcode.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/jsMain/resources/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | overflow: hidden; 3 | margin: 0 !important; 4 | padding: 0 !important; 5 | } 6 | 7 | body > canvas { 8 | outline: none; 9 | } 10 | -------------------------------------------------------------------------------- /demo/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-hdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-hdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-hdpi/ic_launcher_monochrome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-hdpi/ic_launcher_monochrome.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-mdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-mdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-mdpi/ic_launcher_monochrome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-mdpi/ic_launcher_monochrome.webp -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /demo/iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import AVFoundation 3 | 4 | @main 5 | struct iOSApp: App { 6 | var body: some Scene { 7 | WindowGroup { 8 | ContentView() 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tts/src/jsMain/kotlin/js_interop/SpeechSynthesisVoices.kt: -------------------------------------------------------------------------------- 1 | package js_interop 2 | 3 | import org.w3c.speech.SpeechSynthesis 4 | 5 | /** @hide */ 6 | actual fun getVoiceList(speechSynthesis: SpeechSynthesis) = speechSynthesis.getVoices() 7 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-xhdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-xhdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-xhdpi/ic_launcher_monochrome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-xhdpi/ic_launcher_monochrome.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-xxhdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-xxhdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-xxhdpi/ic_launcher_monochrome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-xxhdpi/ic_launcher_monochrome.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-xxxhdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-xxxhdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-xxxhdpi/ic_launcher_monochrome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marc-JB/TextToSpeechKt/HEAD/demo/compose-multiplatform/src/androidMain/resources/mipmap-xxxhdpi/ic_launcher_monochrome.webp -------------------------------------------------------------------------------- /.idea/AndroidProjectSystem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | -------------------------------------------------------------------------------- /tts/src/webCommonW3CMain/kotlin/js_interop/SpeechSynthesisVoices.kt: -------------------------------------------------------------------------------- 1 | package js_interop 2 | 3 | import org.w3c.speech.SpeechSynthesis 4 | import org.w3c.speech.SpeechSynthesisVoice 5 | 6 | /** @hide */ 7 | expect fun getVoiceList(speechSynthesis: SpeechSynthesis): Array 8 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #9e47ff 4 | #6200EE 5 | #66fff8 6 | 7 | -------------------------------------------------------------------------------- /tts-compose/src/commonMain/kotlin/nl/marc_apps/tts/TextToSpeech.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | expect fun rememberTextToSpeechOrNull(requestedEngine: TextToSpeechEngine = TextToSpeechEngine.SystemDefault): TextToSpeechInstance? 7 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /tts/src/androidMain/kotlin/nl/marc_apps/tts/TextToSpeechSystemSettings.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import android.content.Intent 4 | 5 | object TextToSpeechSystemSettings { 6 | const val ACTION_TTS_SETTINGS = "com.android.settings.TTS_SETTINGS" 7 | 8 | fun getIntent() = Intent(ACTION_TTS_SETTINGS) 9 | } -------------------------------------------------------------------------------- /tts/src/jvmMain/kotlin/nl/marc_apps/tts/Voice.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 4 | import java.io.Serializable 5 | import java.util.* 6 | 7 | @ExperimentalVoiceApi 8 | actual interface Voice : CommonVoice, Serializable { 9 | val locale: Locale 10 | } -------------------------------------------------------------------------------- /demo/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tts/src/webCommonW3CMain/kotlin/js_interop/Window.kt: -------------------------------------------------------------------------------- 1 | package js_interop 2 | 3 | import org.w3c.speech.SpeechSynthesis 4 | 5 | expect abstract class Window 6 | 7 | expect val window: Window 8 | 9 | /** @hide */ 10 | expect fun getSpeechSynthesis(window: Window): SpeechSynthesis 11 | 12 | /** @hide */ 13 | expect val isSpeechSynthesisSupported: Boolean 14 | -------------------------------------------------------------------------------- /tts/src/androidMain/kotlin/nl/marc_apps/tts/Voice.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import android.os.Parcelable 4 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 5 | import java.io.Serializable 6 | import java.util.* 7 | 8 | @ExperimentalVoiceApi 9 | actual interface Voice : CommonVoice, Parcelable, Serializable { 10 | val locale: Locale 11 | } -------------------------------------------------------------------------------- /tts/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/commonMain/kotlin/nl/marc_apps/tts_demo/AppTheme.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | fun AppTheme(content: @Composable () -> Unit) { 8 | MaterialTheme( 9 | colorScheme = appColorScheme(), 10 | content = content 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /tts/src/jvmMain/kotlin/nl/marc_apps/tts/utils/SynthesisScope.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.utils 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.Job 6 | 7 | fun SynthesisScope(supervisorJob: Job): CoroutineScope { 8 | val dispatcher = Dispatchers.Default.limitedParallelism(1) 9 | return CoroutineScope(supervisorJob + dispatcher) 10 | } 11 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/commonMain/kotlin/nl/marc_apps/tts_demo/ColorSchemeUtils.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import androidx.compose.material3.ColorScheme 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | expect fun dynamicDarkColorScheme(): ColorScheme 8 | 9 | @Composable 10 | expect fun dynamicLightColorScheme(): ColorScheme 11 | 12 | @Composable 13 | expect fun supportsDynamicColorScheme(): Boolean 14 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/macosMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.Window 2 | import nl.marc_apps.tts_demo.TtsDemoApp 3 | import nl.marc_apps.tts_demo.resources.Res 4 | import nl.marc_apps.tts_demo.resources.app_name 5 | import org.jetbrains.compose.resources.stringResource 6 | 7 | fun main() { 8 | Window(title = "TTS Demo") { 9 | window.title = stringResource(Res.string.app_name) 10 | TtsDemoApp() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/UtteranceResult.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import nl.marc_apps.tts.experimental.ExperimentalTextToSpeechApi 4 | import kotlin.uuid.ExperimentalUuidApi 5 | import kotlin.uuid.Uuid 6 | 7 | /** 8 | * @hide 9 | */ 10 | @OptIn(ExperimentalUuidApi::class) 11 | @ExperimentalTextToSpeechApi 12 | class UtteranceResult(val id: Uuid) { 13 | suspend fun awaitCompletion() { 14 | TODO() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/Voice.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 4 | 5 | /** 6 | * @hide 7 | */ 8 | interface CommonVoice { 9 | val name: String 10 | val isDefault: Boolean 11 | val isOnline: Boolean 12 | val languageTag: String 13 | val language: String 14 | val region: String? 15 | } 16 | 17 | @ExperimentalVoiceApi 18 | expect interface Voice : CommonVoice -------------------------------------------------------------------------------- /tts/src/jsMain/kotlin/js_interop/Window.kt: -------------------------------------------------------------------------------- 1 | package js_interop 2 | 3 | import org.w3c.speech.speechSynthesis 4 | 5 | actual typealias Window = org.w3c.dom.Window 6 | 7 | actual val window: Window = kotlinx.browser.window 8 | 9 | /** @hide */ 10 | actual fun getSpeechSynthesis(window: Window) = window.speechSynthesis 11 | 12 | /** @hide */ 13 | actual val isSpeechSynthesisSupported: Boolean 14 | get() = js("\"speechSynthesis\" in window") as Boolean 15 | -------------------------------------------------------------------------------- /tts/src/androidMain/kotlin/nl/marc_apps/tts/utils/IdUtils.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.utils 2 | 3 | import kotlin.uuid.ExperimentalUuidApi 4 | import kotlin.uuid.Uuid 5 | 6 | /** 7 | * @hide 8 | */ 9 | @OptIn(ExperimentalUuidApi::class) 10 | fun getContinuationId(utteranceId: String?): Uuid? { 11 | return if (utteranceId == null) null else try { 12 | return Uuid.parse(utteranceId) 13 | } catch (_: Throwable) { 14 | null 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/jvmMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.singleWindowApplication 2 | import nl.marc_apps.tts_demo.TtsDemoApp 3 | import nl.marc_apps.tts_demo.resources.Res 4 | import nl.marc_apps.tts_demo.resources.app_name 5 | import org.jetbrains.compose.resources.getString 6 | 7 | suspend fun main() { 8 | singleWindowApplication( 9 | title = getString(Res.string.app_name) 10 | ) { 11 | TtsDemoApp() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/iosMain/kotlin/nl/marc_apps/tts_demo/ColorSchemeUtils.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import androidx.compose.material3.darkColorScheme 4 | import androidx.compose.material3.lightColorScheme 5 | import androidx.compose.runtime.Composable 6 | 7 | @Composable 8 | actual fun dynamicDarkColorScheme() = darkColorScheme() 9 | 10 | @Composable 11 | actual fun dynamicLightColorScheme() = lightColorScheme() 12 | 13 | @Composable 14 | actual fun supportsDynamicColorScheme() = false 15 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/jvmMain/kotlin/nl/marc_apps/tts_demo/ColorSchemeUtils.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import androidx.compose.material3.darkColorScheme 4 | import androidx.compose.material3.lightColorScheme 5 | import androidx.compose.runtime.Composable 6 | 7 | @Composable 8 | actual fun dynamicDarkColorScheme() = darkColorScheme() 9 | 10 | @Composable 11 | actual fun dynamicLightColorScheme() = lightColorScheme() 12 | 13 | @Composable 14 | actual fun supportsDynamicColorScheme() = false 15 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/macosMain/kotlin/nl/marc_apps/tts_demo/ColorSchemeUtils.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import androidx.compose.material3.darkColorScheme 4 | import androidx.compose.material3.lightColorScheme 5 | import androidx.compose.runtime.Composable 6 | 7 | @Composable 8 | actual fun dynamicDarkColorScheme() = darkColorScheme() 9 | 10 | @Composable 11 | actual fun dynamicLightColorScheme() = lightColorScheme() 12 | 13 | @Composable 14 | actual fun supportsDynamicColorScheme() = false 15 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/TextToSpeechEngine.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | sealed interface TextToSpeechEngine { 4 | val androidPackage: String? 5 | 6 | data object SystemDefault : TextToSpeechEngine { 7 | override val androidPackage = null 8 | } 9 | 10 | data object Google : TextToSpeechEngine { 11 | override val androidPackage = "com.google.android.tts" 12 | } 13 | 14 | data class Custom(override val androidPackage: String) : TextToSpeechEngine 15 | } 16 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/webCommonW3CMain/kotlin/nl/marc_apps/tts_demo/ColorSchemeUtils.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import androidx.compose.material3.darkColorScheme 4 | import androidx.compose.material3.lightColorScheme 5 | import androidx.compose.runtime.Composable 6 | 7 | @Composable 8 | actual fun dynamicDarkColorScheme() = darkColorScheme() 9 | 10 | @Composable 11 | actual fun dynamicLightColorScheme() = lightColorScheme() 12 | 13 | @Composable 14 | actual fun supportsDynamicColorScheme() = false 15 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/TextToSpeechFactory.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | /** 4 | * Factory class to create a Text-to-Speech instance. 5 | */ 6 | expect class TextToSpeechFactory { 7 | val isSupported: Boolean 8 | 9 | val canChangeVolume: Boolean 10 | 11 | suspend fun create(): Result 12 | 13 | @Throws(RuntimeException::class) 14 | suspend fun createOrThrow(): TextToSpeechInstance 15 | 16 | suspend fun createOrNull(): TextToSpeechInstance? 17 | } 18 | -------------------------------------------------------------------------------- /demo/iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import ComposeApp 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | MainViewControllerKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView().ignoresSafeArea(.all) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | google() 7 | mavenCentral() 8 | } 9 | } 10 | 11 | dependencyResolutionManagement { 12 | @Suppress("UnstableApiUsage") 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | } 18 | 19 | rootProject.name = "TextToSpeechKt" 20 | 21 | include(":tts") 22 | include(":tts-compose") 23 | include(":demo:compose-multiplatform") 24 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/layout/activity_main_xml.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/drawable/ic_volume_mute.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /tts/src/wasmJsMain/kotlin/js_interop/SpeechSynthesisVoices.kt: -------------------------------------------------------------------------------- 1 | package js_interop 2 | 3 | import org.w3c.speech.SpeechSynthesis 4 | import org.w3c.speech.SpeechSynthesisVoice 5 | 6 | /** @hide */ 7 | actual fun getVoiceList(speechSynthesis: SpeechSynthesis) = speechSynthesis.getVoices().toArray() 8 | 9 | /** 10 | * Returns a new [Array] containing all the elements of this [JsArray]. 11 | */ 12 | private fun JsArray.toArray(): Array { 13 | @Suppress("UNCHECKED_CAST", "TYPE_PARAMETER_AS_REIFIED") 14 | return Array(this.length) { this[it] as T } 15 | } 16 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/experimental/ExperimentalIosTarget.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.experimental 2 | 3 | @Deprecated("The iOS/Apple text-to-speech target is no longer experimental. You can remove this annotation.") 4 | @Retention(AnnotationRetention.BINARY) 5 | @Target( 6 | AnnotationTarget.CLASS, 7 | AnnotationTarget.FUNCTION, 8 | AnnotationTarget.ANNOTATION_CLASS, 9 | AnnotationTarget.CONSTRUCTOR, 10 | AnnotationTarget.PROPERTY, 11 | AnnotationTarget.PROPERTY_GETTER, 12 | AnnotationTarget.PROPERTY_SETTER 13 | ) 14 | annotation class ExperimentalIOSTarget 15 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/experimental/ExperimentalWasmTarget.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.experimental 2 | 3 | @Deprecated("The wasm text-to-speech target is no longer experimental. You can remove this annotation.") 4 | @Retention(AnnotationRetention.BINARY) 5 | @Target( 6 | AnnotationTarget.CLASS, 7 | AnnotationTarget.FUNCTION, 8 | AnnotationTarget.ANNOTATION_CLASS, 9 | AnnotationTarget.CONSTRUCTOR, 10 | AnnotationTarget.PROPERTY, 11 | AnnotationTarget.PROPERTY_GETTER, 12 | AnnotationTarget.PROPERTY_SETTER 13 | ) 14 | annotation class ExperimentalWasmTarget 15 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/kotlin/nl/marc_apps/tts_demo/MainXmlActivity.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import android.os.Bundle 4 | import androidx.activity.enableEdgeToEdge 5 | import androidx.appcompat.app.AppCompatActivity 6 | import nl.marc_apps.tts_demo.databinding.ActivityMainXmlBinding 7 | 8 | class MainXmlActivity : AppCompatActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | enableEdgeToEdge() 11 | super.onCreate(savedInstanceState) 12 | setContentView(ActivityMainXmlBinding.inflate(layoutInflater).root) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/jsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TTS demo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tts-compose/src/appleMain/kotlin/nl/marc_apps/tts/TextToSpeech.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import androidx.compose.runtime.* 4 | 5 | @Composable 6 | actual fun rememberTextToSpeechOrNull(requestedEngine: TextToSpeechEngine): TextToSpeechInstance? { 7 | var textToSpeech by remember { mutableStateOf(null) } 8 | 9 | LaunchedEffect(Unit) { 10 | textToSpeech = TextToSpeechFactory().createOrNull() 11 | } 12 | 13 | DisposableEffect(Unit) { 14 | onDispose { 15 | textToSpeech?.close() 16 | textToSpeech = null 17 | } 18 | } 19 | 20 | return textToSpeech 21 | } 22 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/experimental/ExperimentalVoiceApi.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.experimental 2 | 3 | @RequiresOptIn( 4 | message = "This API is experimental. It may be changed in the future without notice.", 5 | level = RequiresOptIn.Level.ERROR 6 | ) 7 | @Retention(AnnotationRetention.BINARY) 8 | @Target( 9 | AnnotationTarget.CLASS, 10 | AnnotationTarget.FUNCTION, 11 | AnnotationTarget.ANNOTATION_CLASS, 12 | AnnotationTarget.CONSTRUCTOR, 13 | AnnotationTarget.PROPERTY, 14 | AnnotationTarget.PROPERTY_GETTER, 15 | AnnotationTarget.PROPERTY_SETTER 16 | ) 17 | annotation class ExperimentalVoiceApi 18 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/kotlin/nl/marc_apps/tts_demo/di/MainModule.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo.di 2 | 3 | import android.content.Context 4 | import nl.marc_apps.tts.TextToSpeechEngine 5 | import nl.marc_apps.tts.TextToSpeechFactory 6 | import org.koin.core.annotation.ComponentScan 7 | import org.koin.core.annotation.Module 8 | import org.koin.core.annotation.Single 9 | 10 | @Module 11 | @ComponentScan("nl.marc_apps.tts_demo.ui") 12 | class MainModule { 13 | @Single 14 | fun textToSpeechFactory(context: Context): TextToSpeechFactory { 15 | return TextToSpeechFactory(context, TextToSpeechEngine.Google) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/kotlin/nl/marc_apps/tts_demo/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import android.app.Application 4 | import nl.marc_apps.tts_demo.di.MainModule 5 | import org.koin.android.ext.koin.androidContext 6 | import org.koin.android.ext.koin.androidLogger 7 | import org.koin.androix.startup.KoinStartup.onKoinStartup 8 | import org.koin.ksp.generated.module 9 | 10 | class MainApplication : Application() { 11 | init { 12 | onKoinStartup { 13 | androidContext(this@MainApplication) 14 | androidLogger() 15 | modules(MainModule().module) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tts-compose/src/webCommonW3CMain/kotlin/nl/marc_apps/tts/TextToSpeech.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import androidx.compose.runtime.* 4 | 5 | @Composable 6 | actual fun rememberTextToSpeechOrNull(requestedEngine: TextToSpeechEngine): TextToSpeechInstance? { 7 | var textToSpeech by remember { mutableStateOf(null) } 8 | 9 | LaunchedEffect(Unit) { 10 | textToSpeech = TextToSpeechFactory().createOrNull() 11 | } 12 | 13 | DisposableEffect(Unit) { 14 | onDispose { 15 | textToSpeech?.close() 16 | textToSpeech = null 17 | } 18 | } 19 | 20 | return textToSpeech 21 | } 22 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/experimental/ExperimentalDesktopTarget.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.experimental 2 | 3 | @RequiresOptIn( 4 | message = "This API is experimental. It may be changed in the future without notice.", 5 | level = RequiresOptIn.Level.ERROR 6 | ) 7 | @Retention(AnnotationRetention.BINARY) 8 | @Target( 9 | AnnotationTarget.CLASS, 10 | AnnotationTarget.FUNCTION, 11 | AnnotationTarget.ANNOTATION_CLASS, 12 | AnnotationTarget.CONSTRUCTOR, 13 | AnnotationTarget.PROPERTY, 14 | AnnotationTarget.PROPERTY_GETTER, 15 | AnnotationTarget.PROPERTY_SETTER 16 | ) 17 | annotation class ExperimentalDesktopTarget 18 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/experimental/ExperimentalTextToSpeechApi.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.experimental 2 | 3 | @RequiresOptIn( 4 | message = "This API is experimental. It may be changed in the future without notice.", 5 | level = RequiresOptIn.Level.ERROR 6 | ) 7 | @Retention(AnnotationRetention.BINARY) 8 | @Target( 9 | AnnotationTarget.CLASS, 10 | AnnotationTarget.FUNCTION, 11 | AnnotationTarget.ANNOTATION_CLASS, 12 | AnnotationTarget.CONSTRUCTOR, 13 | AnnotationTarget.PROPERTY, 14 | AnnotationTarget.PROPERTY_GETTER, 15 | AnnotationTarget.PROPERTY_SETTER 16 | ) 17 | annotation class ExperimentalTextToSpeechApi 18 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/wasmJsMain/resources/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | overflow: hidden; 3 | width: 100%; 4 | height: 100%; 5 | margin: 0 !important; 6 | padding: 0 !important; 7 | } 8 | 9 | body > canvas { 10 | outline: none; 11 | } 12 | 13 | #warning { 14 | position: absolute; 15 | top: 100px; 16 | left: 100px; 17 | max-width: 830px; 18 | z-index: 100; 19 | background-color: white; 20 | font-size: initial; 21 | display: none; 22 | } 23 | #warning li { 24 | padding-bottom: 15px; 25 | } 26 | 27 | #warning span.code { 28 | font-family: monospace; 29 | } 30 | 31 | ul { 32 | margin-top: 0; 33 | margin-bottom: 15px; 34 | } 35 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/wasmJsMain/kotlin/Main.wasm.kt: -------------------------------------------------------------------------------- 1 | 2 | import androidx.compose.runtime.LaunchedEffect 3 | import androidx.compose.ui.ExperimentalComposeUiApi 4 | import androidx.compose.ui.window.CanvasBasedWindow 5 | import kotlinx.browser.window 6 | import nl.marc_apps.tts_demo.TtsDemoApp 7 | import nl.marc_apps.tts_demo.resources.Res 8 | import nl.marc_apps.tts_demo.resources.app_name 9 | import org.jetbrains.compose.resources.getString 10 | 11 | @OptIn(ExperimentalComposeUiApi::class) 12 | fun main() { 13 | CanvasBasedWindow { 14 | TtsDemoApp() 15 | 16 | LaunchedEffect(Unit) { 17 | window.document.title = getString(Res.string.app_name) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | TTS Demo 4 | Text-to-Speech is not available. 5 | Synthesising 6 | Not active 7 | Text to synthesise 8 | Say! 9 | Options 10 | Unknown voice 11 | Reset all options 12 | 13 | -------------------------------------------------------------------------------- /tts/src/appleMain/kotlin/nl/marc_apps/tts/TextToSpeechFactory.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import platform.AVFAudio.AVSpeechSynthesizer 4 | 5 | actual class TextToSpeechFactory { 6 | actual val isSupported = true 7 | 8 | actual val canChangeVolume = false 9 | 10 | actual suspend fun create(): Result { 11 | return Result.success(TextToSpeechIOS(AVSpeechSynthesizer())) 12 | } 13 | 14 | @Throws(RuntimeException::class) 15 | actual suspend fun createOrThrow(): TextToSpeechInstance { 16 | return create().getOrThrow() 17 | } 18 | 19 | actual suspend fun createOrNull(): TextToSpeechInstance? { 20 | return create().getOrNull() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/commonMain/composeResources/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | TTS Demo 4 | Text-to-Speech is not available. 5 | Synthesising 6 | Not active 7 | Text to synthesise 8 | Say! 9 | Options 10 | Unknown voice 11 | Reset all options 12 | 13 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/commonMain/composeResources/values-en-rUS/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | TTS Demo 4 | Text-to-Speech is not available. 5 | Synthesizing 6 | Not active 7 | Text to synthesize 8 | Say! 9 | Options 10 | Unknown voice 11 | Reset all options 12 | 13 | -------------------------------------------------------------------------------- /tts-compose/src/jvmMain/kotlin/nl/marc_apps/tts/TextToSpeech.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import androidx.compose.runtime.* 4 | import nl.marc_apps.tts.experimental.ExperimentalDesktopTarget 5 | 6 | @ExperimentalDesktopTarget 7 | @Composable 8 | actual fun rememberTextToSpeechOrNull(requestedEngine: TextToSpeechEngine): TextToSpeechInstance? { 9 | var textToSpeech by remember { mutableStateOf(null) } 10 | 11 | LaunchedEffect(Unit) { 12 | textToSpeech = TextToSpeechFactory().createOrNull() 13 | } 14 | 15 | DisposableEffect(Unit) { 16 | onDispose { 17 | textToSpeech?.close() 18 | textToSpeech = null 19 | } 20 | } 21 | 22 | return textToSpeech 23 | } 24 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/commonMain/composeResources/values-nl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | TTS Demo 4 | Tekst-naar-spraak is niet beschikbaar. 5 | Aan het uitspreken 6 | Niet actief 7 | Tekst om uit te spreken 8 | Spreek uit! 9 | Opties 10 | Onbekende stem 11 | Alle opties resetten 12 | 13 | -------------------------------------------------------------------------------- /tts/src/wasmJsMain/kotlin/js_interop/Window.kt: -------------------------------------------------------------------------------- 1 | package js_interop 2 | 3 | import org.w3c.speech.SpeechSynthesis 4 | 5 | actual typealias Window = org.w3c.dom.Window 6 | 7 | actual val window: Window = kotlinx.browser.window 8 | 9 | /** @hide */ 10 | @JsFun("function getBrowserSynthesis() { return window.speechSynthesis; }") 11 | external fun getBrowserSynthesis(): SpeechSynthesis 12 | 13 | /** @hide */ 14 | actual fun getSpeechSynthesis(window: Window): SpeechSynthesis = getBrowserSynthesis() 15 | 16 | /** @hide */ 17 | @JsFun("function getIsSpeechSynthesisSupported() { return \"speechSynthesis\" in window; }") 18 | external fun getIsSpeechSynthesisSupported(): Boolean 19 | 20 | /** @hide */ 21 | actual val isSpeechSynthesisSupported: Boolean 22 | get() = getIsSpeechSynthesisSupported() 23 | -------------------------------------------------------------------------------- /tts-compose/src/androidMain/kotlin/nl/marc_apps/tts/TextToSpeech.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import androidx.compose.runtime.* 4 | import androidx.compose.ui.platform.LocalContext 5 | 6 | @Composable 7 | actual fun rememberTextToSpeechOrNull(requestedEngine: TextToSpeechEngine): TextToSpeechInstance? { 8 | val context = LocalContext.current.applicationContext 9 | 10 | var textToSpeech by remember { mutableStateOf(null) } 11 | 12 | LaunchedEffect(Unit) { 13 | textToSpeech = TextToSpeechFactory(context, requestedEngine).createOrNull() 14 | } 15 | 16 | DisposableEffect(Unit) { 17 | onDispose { 18 | textToSpeech?.close() 19 | textToSpeech = null 20 | } 21 | } 22 | 23 | return textToSpeech 24 | } 25 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/commonMain/kotlin/nl/marc_apps/tts_demo/AppColorScheme.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.ColorScheme 5 | import androidx.compose.material3.darkColorScheme 6 | import androidx.compose.material3.lightColorScheme 7 | import androidx.compose.runtime.Composable 8 | 9 | @Composable 10 | fun appColorScheme(): ColorScheme { 11 | val darkTheme = isSystemInDarkTheme() 12 | val dynamicColor = supportsDynamicColorScheme() 13 | return when { 14 | dynamicColor && darkTheme -> dynamicDarkColorScheme() 15 | dynamicColor && !darkTheme -> dynamicLightColorScheme() 16 | darkTheme -> darkColorScheme() 17 | else -> lightColorScheme() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/jsMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.LaunchedEffect 2 | import androidx.compose.ui.ExperimentalComposeUiApi 3 | import androidx.compose.ui.window.CanvasBasedWindow 4 | import kotlinx.browser.window 5 | import nl.marc_apps.tts_demo.TtsDemoApp 6 | import nl.marc_apps.tts_demo.resources.Res 7 | import nl.marc_apps.tts_demo.resources.app_name 8 | import org.jetbrains.compose.resources.getString 9 | import org.jetbrains.skiko.wasm.onWasmReady 10 | 11 | @OptIn(ExperimentalComposeUiApi::class) 12 | fun main() { 13 | onWasmReady { 14 | CanvasBasedWindow { 15 | TtsDemoApp() 16 | 17 | LaunchedEffect(Unit) { 18 | window.document.title = getString(Res.string.app_name) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/utils/ResultHandler.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.utils 2 | 3 | import kotlin.coroutines.Continuation 4 | 5 | sealed interface ResultHandler { 6 | fun setResult(result: Result) 7 | 8 | class ContinuationHandler(private val continuation: Continuation) : ResultHandler { 9 | override fun setResult(result: Result) { 10 | continuation.resumeWith(result) 11 | } 12 | } 13 | 14 | class CallbackHandler(private val callback: (result: Result) -> Unit) : ResultHandler { 15 | override fun setResult(result: Result) { 16 | callback(result) 17 | } 18 | } 19 | 20 | data object Empty : ResultHandler { 21 | override fun setResult(result: Result) {} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tts/src/appleMain/kotlin/nl/marc_apps/tts/utils/ErrorPointerUtils.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.utils 2 | 3 | import kotlinx.cinterop.BetaInteropApi 4 | import kotlinx.cinterop.ExperimentalForeignApi 5 | import kotlinx.cinterop.ObjCObjectVar 6 | import kotlinx.cinterop.alloc 7 | import kotlinx.cinterop.memScoped 8 | import kotlinx.cinterop.value 9 | import platform.Foundation.NSError 10 | 11 | object ErrorPointerUtils { 12 | @OptIn(ExperimentalForeignApi::class) 13 | fun createErrorPointer(block: (ObjCObjectVar) -> T): T = memScoped { 14 | block(alloc>()) 15 | } 16 | } 17 | 18 | @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class) 19 | fun ObjCObjectVar.throwOnError() { 20 | if (value != null) { 21 | throw RuntimeException(value?.debugDescription) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/jvmMain/kotlin/nl/marc_apps/tts_demo/TtsDemoViewPreview.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import androidx.compose.desktop.ui.tooling.preview.Preview 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material3.Scaffold 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import nl.marc_apps.tts.TextToSpeechEngine 9 | import nl.marc_apps.tts.rememberTextToSpeechOrNull 10 | 11 | @Preview 12 | @Composable 13 | private fun TtsDemoViewPreview() { 14 | val textToSpeech = rememberTextToSpeechOrNull(TextToSpeechEngine.Google) 15 | 16 | AppTheme { 17 | Scaffold( 18 | modifier = Modifier.fillMaxSize() 19 | ) { 20 | TtsDemoView(textToSpeech = textToSpeech, paddingValues = it) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/drawable/ic_volume_up.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/commonMain/kotlin/nl/marc_apps/tts_demo/TtsDemoApp.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Scaffold 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import nl.marc_apps.tts.TextToSpeechEngine 9 | import nl.marc_apps.tts.rememberTextToSpeechOrNull 10 | 11 | @Composable 12 | fun TtsDemoApp(topAppBar: @Composable () -> Unit = {}) { 13 | val textToSpeech = rememberTextToSpeechOrNull(TextToSpeechEngine.Google) 14 | 15 | AppTheme { 16 | Scaffold( 17 | topBar = topAppBar, 18 | modifier = Modifier.fillMaxSize() 19 | ) { 20 | TtsDemoView(textToSpeech = textToSpeech, paddingValues = it) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tts/src/webCommonW3CMain/kotlin/nl/marc_apps/tts/BrowserVoice.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 4 | import org.w3c.speech.SpeechSynthesisVoice 5 | 6 | @ExperimentalVoiceApi 7 | internal data class BrowserVoice( 8 | override val name: String, 9 | override val isDefault: Boolean, 10 | override val isOnline: Boolean, 11 | override val languageTag: String, 12 | override val language: String, 13 | override val region: String?, 14 | val browserVoice: SpeechSynthesisVoice 15 | ) : Voice { 16 | constructor(voice: SpeechSynthesisVoice) : this( 17 | voice.name, 18 | voice.default, 19 | !voice.localService, 20 | voice.lang, 21 | voice.lang.substringBefore("-"), 22 | if("-" in voice.lang) voice.lang.substringAfter("-") else null, 23 | voice 24 | ) 25 | } -------------------------------------------------------------------------------- /tts/src/appleMain/kotlin/nl/marc_apps/tts/IOSVoice.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 4 | import platform.AVFAudio.AVSpeechSynthesisVoice 5 | 6 | @ExperimentalVoiceApi 7 | internal data class IOSVoice( 8 | override val name: String, 9 | override val isDefault: Boolean, 10 | override val isOnline: Boolean, 11 | override val languageTag: String, 12 | override val language: String, 13 | override val region: String?, 14 | val iosVoice: AVSpeechSynthesisVoice 15 | ) : Voice { 16 | constructor(voice: AVSpeechSynthesisVoice, isDefault: Boolean) : this( 17 | voice.name, 18 | isDefault, 19 | false, 20 | voice.language, 21 | voice.language.substringBefore("-"), 22 | if("-" in voice.language) voice.language.substringAfter("-") else null, 23 | voice 24 | ) 25 | } -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/errors/TextToSpeechInitialisationErrors.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.errors 2 | 3 | /** Error that is thrown when creating a [nl.marc_apps.tts.TextToSpeechInstance] fails. */ 4 | sealed class TextToSpeechInitialisationError( 5 | message: String? = "Error while trying to load Text-to-Speech service", 6 | cause: Throwable? = null 7 | ) : Exception(message, cause) 8 | 9 | /** Error that is thrown when creating a [nl.marc_apps.tts.TextToSpeechInstance] fails. */ 10 | class UnknownTextToSpeechInitialisationError( 11 | cause: Throwable? = null 12 | ) : TextToSpeechInitialisationError(cause = cause) 13 | 14 | /** Error that is thrown when a platform does not have TTS support */ 15 | class TextToSpeechNotSupportedError( 16 | cause: Throwable? = null 17 | ) : TextToSpeechInitialisationError("Text-to-Speech is not supported on this platform", cause) 18 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/kotlin/nl/marc_apps/tts_demo/ColorSchemeUtils.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import android.os.Build 4 | import androidx.compose.material3.* 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.platform.LocalContext 7 | 8 | @Composable 9 | actual fun dynamicDarkColorScheme(): ColorScheme { 10 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 11 | dynamicDarkColorScheme(LocalContext.current) 12 | } else { 13 | darkColorScheme() 14 | } 15 | } 16 | 17 | @Composable 18 | actual fun dynamicLightColorScheme(): ColorScheme { 19 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 20 | dynamicLightColorScheme(LocalContext.current) 21 | } else { 22 | lightColorScheme() 23 | } 24 | } 25 | 26 | @Composable 27 | actual fun supportsDynamicColorScheme() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S 28 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/kotlin/nl/marc_apps/tts_demo/TtsDemoViewPreview.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.material3.Scaffold 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.tooling.preview.Preview 8 | import nl.marc_apps.tts.TextToSpeechEngine 9 | import nl.marc_apps.tts.experimental.ExperimentalDesktopTarget 10 | import nl.marc_apps.tts.rememberTextToSpeechOrNull 11 | 12 | @OptIn(ExperimentalDesktopTarget::class) 13 | @Preview 14 | @Composable 15 | private fun TtsDemoViewPreview() { 16 | val textToSpeech = rememberTextToSpeechOrNull(TextToSpeechEngine.Google) 17 | 18 | AppTheme { 19 | Scaffold( 20 | modifier = Modifier.fillMaxSize() 21 | ) { 22 | TtsDemoView(textToSpeech = textToSpeech, paddingValues = it) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Create_library_docs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /tts/src/webCommonW3CMain/kotlin/nl/marc_apps/tts/TextToSpeechFactory.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import js_interop.Window 4 | import js_interop.isSpeechSynthesisSupported 5 | import js_interop.window 6 | import nl.marc_apps.tts.errors.TextToSpeechNotSupportedError 7 | 8 | /** 9 | * Factory class to create a Text-to-Speech instance. 10 | */ 11 | actual class TextToSpeechFactory( 12 | private val context: Window = window 13 | ) { 14 | actual val isSupported = isSpeechSynthesisSupported 15 | 16 | actual val canChangeVolume = true 17 | 18 | actual suspend fun create(): Result { 19 | return if (isSupported) { 20 | Result.success(TextToSpeechBrowser(context)) 21 | } else { 22 | Result.failure(TextToSpeechNotSupportedError()) 23 | } 24 | } 25 | 26 | @Throws(RuntimeException::class) 27 | actual suspend fun createOrThrow(): TextToSpeechInstance { 28 | return create().getOrThrow() 29 | } 30 | 31 | actual suspend fun createOrNull(): TextToSpeechInstance? { 32 | return create().getOrNull() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Run_Browser__JS__Compose_Demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Run_Browser__WASM__Compose_Demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /tts/src/jvmMain/kotlin/nl/marc_apps/tts/TextToSpeechFactory.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import com.sun.speech.freetts.VoiceManager 4 | import nl.marc_apps.tts.experimental.ExperimentalDesktopTarget 5 | 6 | @ExperimentalDesktopTarget 7 | actual class TextToSpeechFactory { 8 | @ExperimentalDesktopTarget 9 | actual val isSupported = true 10 | 11 | @ExperimentalDesktopTarget 12 | actual val canChangeVolume = true 13 | 14 | @ExperimentalDesktopTarget 15 | actual suspend fun create(): Result { 16 | System.setProperty("freetts.voices", "com.sun.speech.freetts.en.us.cmu_us_kal.KevinVoiceDirectory") 17 | val voiceManager = VoiceManager.getInstance() 18 | return Result.success(TextToSpeechDesktop(voiceManager)) 19 | } 20 | 21 | @ExperimentalDesktopTarget 22 | @Throws(RuntimeException::class) 23 | actual suspend fun createOrThrow(): TextToSpeechInstance { 24 | return create().getOrThrow() 25 | } 26 | 27 | @ExperimentalDesktopTarget 28 | actual suspend fun createOrNull(): TextToSpeechInstance? { 29 | return create().getOrNull() 30 | } 31 | } -------------------------------------------------------------------------------- /.idea/runConfigurations/Build_library.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 17 | 19 | true 20 | true 21 | false 22 | false 23 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Run_Desktop_Compose_Demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | false 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/validation.yml: -------------------------------------------------------------------------------- 1 | name: Validation 2 | 3 | on: 4 | pull_request: 5 | branches: main 6 | workflow_dispatch: 7 | 8 | concurrency: 9 | group: validation-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build-lib: 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 8 16 | steps: 17 | 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up JDK 17 22 | uses: actions/setup-java@v4 23 | with: 24 | java-version: 17 25 | distribution: 'temurin' 26 | cache: 'gradle' 27 | 28 | - name: Grant execute permission for gradlew 29 | run: chmod +x gradlew 30 | 31 | - name: Build with Gradle 32 | run: ./gradlew :tts:build :tts-compose:build 33 | env: 34 | GPR_USER: github-actions 35 | GPR_KEY: ${{ secrets.GITHUB_TOKEN }} 36 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 37 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 38 | GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} 39 | GPG_SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }} 40 | -------------------------------------------------------------------------------- /tts/src/webCommonW3CMain/kotlin/org/w3c/speech/SpeechSynthesisVoice.kt: -------------------------------------------------------------------------------- 1 | package org.w3c.speech 2 | 3 | /** 4 | * The SpeechSynthesisVoice interface of the Web Speech API represents a voice that the system supports. 5 | * Every SpeechSynthesisVoice has its own relative speech service including information about language, name and URI. 6 | */ 7 | expect interface SpeechSynthesisVoice { 8 | /** 9 | * A [Boolean] indicating whether the voice is the default voice 10 | * for the current app language (true), or not (false.) 11 | */ 12 | val default: Boolean 13 | 14 | /** Returns a BCP 47 language tag indicating the language of the voice. */ 15 | val lang: String 16 | 17 | /** 18 | * A [Boolean] indicating whether the voice is supplied by 19 | * a local speech synthesizer service (true), or a remote speech synthesizer service (false.) 20 | */ 21 | val localService: Boolean 22 | 23 | /** Returns a human-readable name that represents the voice. */ 24 | val name: String 25 | 26 | /** Returns the type of URI and location of the speech synthesis service for this voice. */ 27 | val voiceURI: String 28 | } 29 | -------------------------------------------------------------------------------- /tts/src/iosMain/kotlin/nl/marc_apps/tts/AudioSession.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import kotlinx.cinterop.ExperimentalForeignApi 4 | import kotlinx.cinterop.ptr 5 | import nl.marc_apps.tts.utils.ErrorPointerUtils 6 | import nl.marc_apps.tts.utils.throwOnError 7 | import platform.AVFAudio.AVAudioSession 8 | import platform.AVFAudio.AVAudioSessionCategoryOptionDuckOthers 9 | import platform.AVFAudio.AVAudioSessionCategoryPlayback 10 | import platform.AVFAudio.AVAudioSessionModeDefault 11 | import platform.AVFAudio.setActive 12 | 13 | object AudioSession { 14 | @ExperimentalForeignApi 15 | fun initialiseForTextToSpeech() { 16 | val audioSession = AVAudioSession.sharedInstance() 17 | 18 | ErrorPointerUtils.createErrorPointer { errorPtr -> 19 | audioSession.setCategory(AVAudioSessionCategoryPlayback, 20 | mode = AVAudioSessionModeDefault, 21 | options = AVAudioSessionCategoryOptionDuckOthers, 22 | errorPtr.ptr) 23 | 24 | errorPtr.throwOnError() 25 | 26 | audioSession.setActive(true, errorPtr.ptr) 27 | 28 | errorPtr.throwOnError() 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /tts/src/wasmJsMain/kotlin/org/w3c/speech/SpeechSynthesisVoice.kt: -------------------------------------------------------------------------------- 1 | package org.w3c.speech 2 | 3 | /** 4 | * The SpeechSynthesisVoice interface of the Web Speech API represents a voice that the system supports. 5 | * Every SpeechSynthesisVoice has its own relative speech service including information about language, name and URI. 6 | */ 7 | actual external interface SpeechSynthesisVoice : JsAny { 8 | /** 9 | * A [Boolean] indicating whether the voice is the default voice 10 | * for the current app language (true), or not (false.) 11 | */ 12 | actual val default: Boolean 13 | 14 | /** Returns a BCP 47 language tag indicating the language of the voice. */ 15 | actual val lang: String 16 | 17 | /** 18 | * A [Boolean] indicating whether the voice is supplied by 19 | * a local speech synthesizer service (true), or a remote speech synthesizer service (false.) 20 | */ 21 | actual val localService: Boolean 22 | 23 | /** Returns a human-readable name that represents the voice. */ 24 | actual val name: String 25 | 26 | /** Returns the type of URI and location of the speech synthesis service for this voice. */ 27 | actual val voiceURI: String 28 | } 29 | -------------------------------------------------------------------------------- /tts/src/jsMain/kotlin/org/w3c/speech/SpeechSynthesisVoice.kt: -------------------------------------------------------------------------------- 1 | package org.w3c.speech 2 | 3 | import js_interop.JsAny 4 | 5 | /** 6 | * The SpeechSynthesisVoice interface of the Web Speech API represents a voice that the system supports. 7 | * Every SpeechSynthesisVoice has its own relative speech service including information about language, name and URI. 8 | */ 9 | actual external interface SpeechSynthesisVoice : JsAny { 10 | /** 11 | * A [Boolean] indicating whether the voice is the default voice 12 | * for the current app language (true), or not (false.) 13 | */ 14 | actual val default: Boolean 15 | 16 | /** Returns a BCP 47 language tag indicating the language of the voice. */ 17 | actual val lang: String 18 | 19 | /** 20 | * A [Boolean] indicating whether the voice is supplied by 21 | * a local speech synthesizer service (true), or a remote speech synthesizer service (false.) 22 | */ 23 | actual val localService: Boolean 24 | 25 | /** Returns a human-readable name that represents the voice. */ 26 | actual val name: String 27 | 28 | /** Returns the type of URI and location of the speech synthesis service for this voice. */ 29 | actual val voiceURI: String 30 | } 31 | -------------------------------------------------------------------------------- /tts/src/webCommonW3CMain/kotlin/org/w3c/speech/SpeechSynthesisUtterance.kt: -------------------------------------------------------------------------------- 1 | package org.w3c.speech 2 | 3 | import js_interop.JsAny 4 | 5 | /** 6 | * The SpeechSynthesisUtterance interface of the Web Speech API represents a speech request. 7 | * It contains the content the speech service should read and 8 | * information about how to read it (e.g. language, pitch and volume.) 9 | */ 10 | expect class SpeechSynthesisUtterance { 11 | constructor() 12 | 13 | constructor(text: String) 14 | 15 | /** Gets and sets the language of the utterance. */ 16 | var lang: String 17 | 18 | /** Gets and sets the pitch at which the utterance will be spoken at. */ 19 | var pitch: Float 20 | 21 | /** Gets and sets the speed at which the utterance will be spoken at. */ 22 | var rate: Float 23 | 24 | /** Gets and sets the text that will be synthesised when the utterance is spoken. */ 25 | var text: String 26 | 27 | /** Gets and sets the voice that will be used to speak the utterance. */ 28 | var voice: SpeechSynthesisVoice? 29 | 30 | /** Gets and sets the volume that the utterance will be spoken at. */ 31 | var volume: Float 32 | 33 | var onstart: ((event: JsAny?) -> Unit)? 34 | 35 | var onend: ((event: JsAny?) -> Unit)? 36 | } 37 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/kotlin/nl/marc_apps/tts_demo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.compose.material3.* 8 | import androidx.compose.ui.unit.dp 9 | import org.jetbrains.compose.resources.stringResource 10 | import nl.marc_apps.tts_demo.resources.* 11 | 12 | class MainActivity : ComponentActivity() { 13 | @OptIn(ExperimentalMaterial3Api::class) 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | enableEdgeToEdge() 16 | 17 | super.onCreate(savedInstanceState) 18 | 19 | setContent { 20 | TtsDemoApp( 21 | topAppBar = { 22 | TopAppBar( 23 | title = { 24 | Text(stringResource(Res.string.app_name)) 25 | }, 26 | colors = TopAppBarDefaults.topAppBarColors( 27 | containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) 28 | ) 29 | ) 30 | } 31 | ) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/utils/CallbackHandler.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.utils 2 | 3 | import kotlin.uuid.ExperimentalUuidApi 4 | import kotlin.uuid.Uuid 5 | 6 | @OptIn(ExperimentalUuidApi::class) 7 | class CallbackHandler { 8 | private val resultHandlers = mutableMapOf() 9 | 10 | private val utteranceIds = mutableMapOf() 11 | 12 | private var queueSize = 0 13 | 14 | val isQueueEmpty 15 | get() = queueSize == 0 16 | 17 | fun add(utteranceId: Uuid, nativeObject: TNativeUtteranceId, resultHandler: ResultHandler) { 18 | resultHandlers[utteranceId] = resultHandler 19 | utteranceIds[nativeObject] = utteranceId 20 | queueSize++ 21 | } 22 | 23 | fun getUtteranceId(nativeObject: TNativeUtteranceId): Uuid? = utteranceIds[nativeObject] 24 | 25 | fun onResult(utteranceId: Uuid, result: Result) { 26 | if (queueSize-- < 0) { 27 | queueSize = 0 28 | } 29 | 30 | val continuation = resultHandlers.remove(utteranceId) 31 | continuation?.setResult(result) 32 | } 33 | 34 | fun onStopped() { 35 | queueSize = 0 36 | } 37 | 38 | fun clear() { 39 | resultHandlers.clear() 40 | queueSize = 0 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tts/src/wasmJsMain/kotlin/org/w3c/speech/SpeechSynthesisUtterance.kt: -------------------------------------------------------------------------------- 1 | package org.w3c.speech 2 | 3 | import org.w3c.dom.events.EventTarget 4 | 5 | /** 6 | * The SpeechSynthesisUtterance interface of the Web Speech API represents a speech request. 7 | * It contains the content the speech service should read and 8 | * information about how to read it (e.g. language, pitch and volume.) 9 | */ 10 | actual external class SpeechSynthesisUtterance : EventTarget { 11 | actual constructor() 12 | 13 | actual constructor(text: String) 14 | 15 | /** Gets and sets the language of the utterance. */ 16 | actual var lang: String 17 | 18 | /** Gets and sets the pitch at which the utterance will be spoken at. */ 19 | actual var pitch: Float 20 | 21 | /** Gets and sets the speed at which the utterance will be spoken at. */ 22 | actual var rate: Float 23 | 24 | /** Gets and sets the text that will be synthesised when the utterance is spoken. */ 25 | actual var text: String 26 | 27 | /** Gets and sets the voice that will be used to speak the utterance. */ 28 | actual var voice: SpeechSynthesisVoice? 29 | 30 | /** Gets and sets the volume that the utterance will be spoken at. */ 31 | actual var volume: Float 32 | 33 | actual var onstart: ((event: JsAny?) -> Unit)? 34 | 35 | actual var onend: ((event: JsAny?) -> Unit)? 36 | } 37 | -------------------------------------------------------------------------------- /tts/src/jsMain/kotlin/org/w3c/speech/SpeechSynthesisUtterance.kt: -------------------------------------------------------------------------------- 1 | package org.w3c.speech 2 | 3 | import js_interop.JsAny 4 | import org.w3c.dom.events.EventTarget 5 | 6 | /** 7 | * The SpeechSynthesisUtterance interface of the Web Speech API represents a speech request. 8 | * It contains the content the speech service should read and 9 | * information about how to read it (e.g. language, pitch and volume.) 10 | */ 11 | actual external class SpeechSynthesisUtterance : EventTarget { 12 | actual constructor() 13 | 14 | actual constructor(text: String) 15 | 16 | /** Gets and sets the language of the utterance. */ 17 | actual var lang: String 18 | 19 | /** Gets and sets the pitch at which the utterance will be spoken at. */ 20 | actual var pitch: Float 21 | 22 | /** Gets and sets the speed at which the utterance will be spoken at. */ 23 | actual var rate: Float 24 | 25 | /** Gets and sets the text that will be synthesised when the utterance is spoken. */ 26 | actual var text: String 27 | 28 | /** Gets and sets the voice that will be used to speak the utterance. */ 29 | actual var voice: SpeechSynthesisVoice? 30 | 31 | /** Gets and sets the volume that the utterance will be spoken at. */ 32 | actual var volume: Float 33 | 34 | actual var onstart: ((event: JsAny?) -> Unit)? 35 | 36 | actual var onend: ((event: JsAny?) -> Unit)? 37 | } 38 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/iosMain/kotlin/nl/marc_apps/tts_demo/MainViewController.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Text 6 | import androidx.compose.material3.TopAppBar 7 | import androidx.compose.material3.TopAppBarDefaults 8 | import androidx.compose.material3.surfaceColorAtElevation 9 | import androidx.compose.runtime.LaunchedEffect 10 | import androidx.compose.ui.unit.dp 11 | import androidx.compose.ui.window.ComposeUIViewController 12 | import kotlinx.cinterop.ExperimentalForeignApi 13 | import nl.marc_apps.tts.AudioSession 14 | import org.jetbrains.compose.resources.stringResource 15 | import nl.marc_apps.tts_demo.resources.* 16 | 17 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalForeignApi::class) 18 | fun MainViewController() = ComposeUIViewController { 19 | LaunchedEffect(Unit) { 20 | AudioSession.initialiseForTextToSpeech() 21 | } 22 | 23 | TtsDemoApp( 24 | topAppBar = { 25 | TopAppBar( 26 | title = { 27 | Text(stringResource(Res.string.app_name)) 28 | }, 29 | colors = TopAppBarDefaults.topAppBarColors( 30 | containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) 31 | ) 32 | ) 33 | } 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 35 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/kotlin/nl/marc_apps/tts_demo/ui/MainXmlViewModel.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo.ui 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.asStateFlow 7 | import kotlinx.coroutines.flow.update 8 | import kotlinx.coroutines.launch 9 | import nl.marc_apps.tts.TextToSpeechFactory 10 | import nl.marc_apps.tts.TextToSpeechInstance 11 | import org.koin.android.annotation.KoinViewModel 12 | 13 | @KoinViewModel 14 | class MainXmlViewModel(private val textToSpeechFactory: TextToSpeechFactory) : ViewModel() { 15 | private var ttsInstance: TextToSpeechInstance? = null 16 | 17 | private val mutableIsTextToSpeechLoaded = MutableStateFlow(false) 18 | val isTextToSpeechLoaded = mutableIsTextToSpeechLoaded.asStateFlow() 19 | 20 | init { 21 | viewModelScope.launch { 22 | initTextToSpeech() 23 | } 24 | } 25 | 26 | fun say(text: String?) { 27 | if (text?.isNotBlank() == true) { 28 | viewModelScope.launch { 29 | ttsInstance?.say(text) 30 | } 31 | } 32 | } 33 | 34 | fun setVolume(volume: Float) { 35 | ttsInstance?.volume = volume.toInt() 36 | } 37 | 38 | private suspend fun initTextToSpeech() { 39 | ttsInstance = textToSpeechFactory.createOrNull() 40 | mutableIsTextToSpeechLoaded.update { true } 41 | } 42 | 43 | override fun onCleared() { 44 | ttsInstance?.close() 45 | ttsInstance = null 46 | super.onCleared() 47 | } 48 | } -------------------------------------------------------------------------------- /tts/src/webCommonW3CMain/kotlin/org/w3c/speech/SpeechSynthesis.kt: -------------------------------------------------------------------------------- 1 | package org.w3c.speech 2 | 3 | import js_interop.JsAny 4 | 5 | /** 6 | * The SpeechSynthesis interface of the Web Speech API is the controller interface for the speech service; 7 | * this can be used to retrieve information about the synthesis voices available on the device, 8 | * start and pause speech, and other commands besides. 9 | */ 10 | expect abstract class SpeechSynthesis { 11 | /** A [Boolean] that returns true if the SpeechSynthesis object is in a paused state. */ 12 | val paused: Boolean 13 | 14 | /** A [Boolean] that returns true if the utterance queue contains as-yet-unspoken utterances. */ 15 | val pending: Boolean 16 | 17 | /** 18 | * A [Boolean] that returns true if an utterance is currently 19 | * in the process of being spoken — even if SpeechSynthesis is in a paused state. 20 | */ 21 | val speaking: Boolean 22 | 23 | /** Removes all utterances from the utterance queue. */ 24 | fun cancel() 25 | 26 | /** Puts the SpeechSynthesis object into a paused state. */ 27 | fun pause() 28 | 29 | /** Puts the SpeechSynthesis object into a non-paused state: resumes it if it was already paused. */ 30 | fun resume() 31 | 32 | /** 33 | * Adds an [utterance] to the utterance queue; 34 | * it will be spoken when any other utterances queued before it have been spoken. 35 | */ 36 | fun speak(utterance: SpeechSynthesisUtterance) 37 | 38 | /** 39 | * Fired when the list of [SpeechSynthesisVoice] objects that would be returned by the [getVoices] method has changed. 40 | */ 41 | var voiceschanged: ((event: JsAny?) -> Unit)? 42 | } 43 | -------------------------------------------------------------------------------- /tts/src/androidMain/kotlin/nl/marc_apps/tts/utils/VoiceAndroidLegacy.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.utils 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | import nl.marc_apps.tts.Voice 6 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 7 | import java.util.* 8 | 9 | @ExperimentalVoiceApi 10 | internal data class VoiceAndroidLegacy( 11 | override val name: String, 12 | override val isDefault: Boolean, 13 | override val isOnline: Boolean, 14 | override val languageTag: String, 15 | override val language: String, 16 | override val region: String?, 17 | override val locale: Locale 18 | ) : Voice { 19 | constructor(parcel: Parcel) : this( 20 | parcel.readSerializable() as Locale, 21 | parcel.readByte() != 0.toByte() 22 | ) 23 | 24 | constructor(locale: Locale, isDefault: Boolean) : this( 25 | locale.displayName, 26 | isDefault, 27 | false, 28 | locale.language, 29 | locale.displayLanguage, 30 | locale.displayCountry, 31 | locale 32 | ) 33 | 34 | override fun equals(other: Any?) = (other as? VoiceAndroidLegacy)?.locale == locale 35 | 36 | override fun hashCode() = locale.hashCode() 37 | override fun writeToParcel(parcel: Parcel, flags: Int) { 38 | parcel.writeSerializable(locale) 39 | parcel.writeByte(if (isDefault) 1 else 0) 40 | } 41 | 42 | override fun describeContents() = 0 43 | 44 | companion object CREATOR : Parcelable.Creator { 45 | override fun createFromParcel(parcel: Parcel) = VoiceAndroidLegacy(parcel) 46 | 47 | override fun newArray(size: Int) = arrayOfNulls(size) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tts/src/androidMain/kotlin/nl/marc_apps/tts/utils/ErrorCodes.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.utils 2 | 3 | import nl.marc_apps.tts.errors.* 4 | 5 | /** 6 | * @hide 7 | */ 8 | object ErrorCodes { 9 | /** Denotes a failure of a TTS engine to synthesize the given input. */ 10 | const val ERROR_SYNTHESIS = -3 11 | 12 | /** Denotes a failure of a TTS service. */ 13 | const val ERROR_SERVICE = -4 14 | 15 | /** Denotes a failure related to the output (audio device or a file). */ 16 | const val ERROR_OUTPUT = -5 17 | 18 | /** Denotes a failure caused by a network connectivity problems. */ 19 | const val ERROR_NETWORK = -6 20 | 21 | /** Denotes a failure caused by network timeout.*/ 22 | const val ERROR_NETWORK_TIMEOUT = -7 23 | 24 | /** Denotes a failure caused by an invalid request. */ 25 | const val ERROR_INVALID_REQUEST = -8 26 | 27 | /** Denotes a failure caused by an unfinished download of the voice data. */ 28 | const val ERROR_NOT_INSTALLED_YET = -9 29 | 30 | /** 31 | * @hide 32 | */ 33 | fun mapToThrowable(errorCode: Int): TextToSpeechSynthesisError { 34 | return when(errorCode) { 35 | ERROR_SYNTHESIS -> TextToSpeechInputError() 36 | ERROR_SERVICE -> TextToSpeechServiceFailureError() 37 | ERROR_OUTPUT -> DeviceAudioOutputError() 38 | ERROR_NETWORK -> TextToSpeechNetworkError(timeout = false) 39 | ERROR_NETWORK_TIMEOUT -> TextToSpeechNetworkError(timeout = true) 40 | ERROR_INVALID_REQUEST -> TextToSpeechInputError() 41 | ERROR_NOT_INSTALLED_YET -> TextToSpeechEngineUnavailableError() 42 | else -> UnknownTextToSpeechSynthesisError() 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tts/src/androidMain/kotlin/nl/marc_apps/tts/utils/TtsProgressConverter.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.utils 2 | 3 | import android.os.Build 4 | import android.speech.tts.UtteranceProgressListener 5 | import androidx.annotation.RequiresApi 6 | import nl.marc_apps.tts.errors.TextToSpeechSynthesisInterruptedError 7 | import nl.marc_apps.tts.errors.UnknownTextToSpeechSynthesisError 8 | import kotlin.uuid.ExperimentalUuidApi 9 | import kotlin.uuid.Uuid 10 | 11 | /** 12 | * @hide 13 | */ 14 | @OptIn(ExperimentalUuidApi::class) 15 | @RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) 16 | class TtsProgressConverter( 17 | val onStart: (Uuid) -> Unit, 18 | val onComplete: (Uuid, Result) -> Unit 19 | ) : UtteranceProgressListener() { 20 | override fun onStart(utteranceId: String?) { 21 | val id = getContinuationId(utteranceId) ?: return 22 | onStart(id) 23 | } 24 | 25 | override fun onDone(utteranceId: String?) { 26 | val id = getContinuationId(utteranceId) ?: return 27 | onComplete(id, Result.success(Unit)) 28 | } 29 | 30 | override fun onError(utteranceId: String?) { 31 | val id = getContinuationId(utteranceId) ?: return 32 | onComplete(id, Result.failure(UnknownTextToSpeechSynthesisError())) 33 | } 34 | 35 | override fun onError(utteranceId: String?, errorCode: Int) { 36 | val id = getContinuationId(utteranceId) ?: return 37 | onComplete(id, Result.failure(ErrorCodes.mapToThrowable(errorCode))) 38 | } 39 | 40 | override fun onStop(utteranceId: String?, interrupted: Boolean) { 41 | val id = getContinuationId(utteranceId) ?: return 42 | onComplete(id, Result.failure(TextToSpeechSynthesisInterruptedError())) 43 | } 44 | } -------------------------------------------------------------------------------- /tts/src/appleMain/kotlin/nl/marc_apps/tts/utils/TtsProgressConverter.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.utils 2 | 3 | import kotlinx.cinterop.ObjCSignatureOverride 4 | import nl.marc_apps.tts.errors.TextToSpeechSynthesisInterruptedError 5 | import platform.AVFAudio.* 6 | import platform.darwin.NSObject 7 | import kotlin.uuid.ExperimentalUuidApi 8 | import kotlin.uuid.Uuid 9 | 10 | @OptIn(ExperimentalUuidApi::class) 11 | class TtsProgressConverter( 12 | private val callbackHandler: CallbackHandler, 13 | private val onStart: (Uuid) -> Unit, 14 | private val onComplete: (Uuid, Result) -> Unit 15 | ) : NSObject(), AVSpeechSynthesizerDelegateProtocol { 16 | @ObjCSignatureOverride 17 | override fun speechSynthesizer(synthesizer: AVSpeechSynthesizer, didStartSpeechUtterance: AVSpeechUtterance) { 18 | val utteranceId = callbackHandler.getUtteranceId(didStartSpeechUtterance) 19 | if (utteranceId != null) { 20 | onStart(utteranceId) 21 | } 22 | } 23 | 24 | @ObjCSignatureOverride 25 | override fun speechSynthesizer(synthesizer: AVSpeechSynthesizer, didFinishSpeechUtterance: AVSpeechUtterance) { 26 | val utteranceId = callbackHandler.getUtteranceId(didFinishSpeechUtterance) 27 | if (utteranceId != null) { 28 | onComplete(utteranceId, Result.success(Unit)) 29 | } 30 | } 31 | 32 | @ObjCSignatureOverride 33 | override fun speechSynthesizer(synthesizer: AVSpeechSynthesizer, didCancelSpeechUtterance: AVSpeechUtterance) { 34 | val utteranceId = callbackHandler.getUtteranceId(didCancelSpeechUtterance) 35 | if (utteranceId != null) { 36 | onComplete(utteranceId, Result.failure(TextToSpeechSynthesisInterruptedError())) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | TTS Demo 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UIApplicationSceneManifest 28 | 29 | UIApplicationSupportsMultipleScenes 30 | 31 | 32 | UILaunchScreen 33 | 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /tts/src/wasmJsMain/kotlin/org/w3c/speech/SpeechSynthesis.kt: -------------------------------------------------------------------------------- 1 | package org.w3c.speech 2 | 3 | import org.w3c.dom.events.EventTarget 4 | 5 | /** 6 | * The SpeechSynthesis interface of the Web Speech API is the controller interface for the speech service; 7 | * this can be used to retrieve information about the synthesis voices available on the device, 8 | * start and pause speech, and other commands besides. 9 | */ 10 | actual abstract external class SpeechSynthesis : EventTarget { 11 | /** A [Boolean] that returns true if the SpeechSynthesis object is in a paused state. */ 12 | actual val paused: Boolean 13 | 14 | /** A [Boolean] that returns true if the utterance queue contains as-yet-unspoken utterances. */ 15 | actual val pending: Boolean 16 | 17 | /** 18 | * A [Boolean] that returns true if an utterance is currently 19 | * in the process of being spoken — even if SpeechSynthesis is in a paused state. 20 | */ 21 | actual val speaking: Boolean 22 | 23 | /** Removes all utterances from the utterance queue. */ 24 | actual fun cancel() 25 | 26 | /** Returns a list of [SpeechSynthesisVoice] objects representing all the available voices on the current device. */ 27 | fun getVoices(): JsArray 28 | 29 | /** Puts the SpeechSynthesis object into a paused state. */ 30 | actual fun pause() 31 | 32 | /** Puts the SpeechSynthesis object into a non-paused state: resumes it if it was already paused. */ 33 | actual fun resume() 34 | 35 | /** 36 | * Adds an [utterance] to the utterance queue; 37 | * it will be spoken when any other utterances queued before it have been spoken. 38 | */ 39 | actual fun speak(utterance: SpeechSynthesisUtterance) 40 | 41 | /** 42 | * Fired when the list of [SpeechSynthesisVoice] objects that would be returned by the [getVoices] method has changed. 43 | */ 44 | actual var voiceschanged: ((event: JsAny?) -> Unit)? 45 | } 46 | -------------------------------------------------------------------------------- /tts/src/jsMain/kotlin/org/w3c/speech/SpeechSynthesis.kt: -------------------------------------------------------------------------------- 1 | package org.w3c.speech 2 | 3 | import js_interop.JsAny 4 | import org.w3c.dom.events.EventTarget 5 | 6 | /** 7 | * The SpeechSynthesis interface of the Web Speech API is the controller interface for the speech service; 8 | * this can be used to retrieve information about the synthesis voices available on the device, 9 | * start and pause speech, and other commands besides. 10 | */ 11 | actual abstract external class SpeechSynthesis : EventTarget { 12 | /** A [Boolean] that returns true if the SpeechSynthesis object is in a paused state. */ 13 | actual val paused: Boolean 14 | 15 | /** A [Boolean] that returns true if the utterance queue contains as-yet-unspoken utterances. */ 16 | actual val pending: Boolean 17 | 18 | /** 19 | * A [Boolean] that returns true if an utterance is currently 20 | * in the process of being spoken — even if SpeechSynthesis is in a paused state. 21 | */ 22 | actual val speaking: Boolean 23 | 24 | /** Removes all utterances from the utterance queue. */ 25 | actual fun cancel() 26 | 27 | /** Returns a list of [SpeechSynthesisVoice] objects representing all the available voices on the current device. */ 28 | fun getVoices(): Array 29 | 30 | /** Puts the SpeechSynthesis object into a paused state. */ 31 | actual fun pause() 32 | 33 | /** Puts the SpeechSynthesis object into a non-paused state: resumes it if it was already paused. */ 34 | actual fun resume() 35 | 36 | /** 37 | * Adds an [utterance] to the utterance queue; 38 | * it will be spoken when any other utterances queued before it have been spoken. 39 | */ 40 | actual fun speak(utterance: SpeechSynthesisUtterance) 41 | 42 | /** 43 | * Fired when the list of [SpeechSynthesisVoice] objects that would be returned by the [getVoices] method has changed. 44 | */ 45 | actual var voiceschanged: ((event: JsAny?) -> Unit)? 46 | } 47 | -------------------------------------------------------------------------------- /tts/src/androidMain/kotlin/nl/marc_apps/tts/utils/VoiceAndroidModern.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.utils 2 | 3 | import android.os.Build 4 | import android.os.Parcel 5 | import android.os.Parcelable 6 | import androidx.annotation.RequiresApi 7 | import nl.marc_apps.tts.Voice 8 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 9 | 10 | @ExperimentalVoiceApi 11 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 12 | internal data class VoiceAndroidModern( 13 | override val name: String, 14 | override val isDefault: Boolean, 15 | override val isOnline: Boolean, 16 | override val languageTag: String, 17 | override val language: String, 18 | override val region: String?, 19 | val androidVoice: android.speech.tts.Voice 20 | ) : Voice { 21 | override val locale = androidVoice.locale 22 | 23 | constructor(parcel: Parcel) : this( 24 | android.speech.tts.Voice.CREATOR.createFromParcel(parcel), 25 | parcel.readByte() != 0.toByte() 26 | ) 27 | 28 | constructor(androidVoice: android.speech.tts.Voice, isDefault: Boolean) : this( 29 | androidVoice.locale.displayName, 30 | isDefault, 31 | androidVoice.isNetworkConnectionRequired, 32 | androidVoice.locale.toLanguageTag(), 33 | androidVoice.locale.displayLanguage, 34 | androidVoice.locale.displayCountry, 35 | androidVoice 36 | ) 37 | 38 | override fun equals(other: Any?) = (other as? VoiceAndroidModern)?.androidVoice == androidVoice 39 | 40 | override fun hashCode() = androidVoice.hashCode() 41 | 42 | override fun writeToParcel(parcel: Parcel, flags: Int) { 43 | androidVoice.writeToParcel(parcel, flags) 44 | parcel.writeByte(if (isDefault) 1 else 0) 45 | } 46 | 47 | override fun describeContents() = 0 48 | 49 | companion object CREATOR : Parcelable.Creator { 50 | override fun createFromParcel(parcel: Parcel) = VoiceAndroidModern(parcel) 51 | 52 | override fun newArray(size: Int) = arrayOfNulls(size) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Run_Android_Compose_Demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 39 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/kotlin/nl/marc_apps/tts_demo/ui/MainXmlFragment.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo.ui 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.lifecycle.Lifecycle 9 | import androidx.lifecycle.lifecycleScope 10 | import androidx.lifecycle.repeatOnLifecycle 11 | import kotlinx.coroutines.launch 12 | import nl.marc_apps.tts_demo.databinding.FragmentMainXmlBinding 13 | import org.koin.androidx.viewmodel.ext.android.viewModel 14 | 15 | class MainXmlFragment : Fragment() { 16 | private lateinit var binding: FragmentMainXmlBinding 17 | 18 | private val viewModel by viewModel() 19 | 20 | override fun onCreateView( 21 | inflater: LayoutInflater, 22 | container: ViewGroup?, 23 | savedInstanceState: Bundle? 24 | ): View { 25 | binding = FragmentMainXmlBinding.inflate(inflater, container, false) 26 | return binding.root 27 | } 28 | 29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 30 | super.onViewCreated(view, savedInstanceState) 31 | 32 | binding.actionSay.isEnabled = false 33 | binding.loadingIndicator.visibility = View.VISIBLE 34 | 35 | viewLifecycleOwner.lifecycleScope.launch { 36 | viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { 37 | viewModel.isTextToSpeechLoaded.collect { hasLoaded -> 38 | binding.actionSay.isEnabled = hasLoaded 39 | binding.loadingIndicator.visibility = if (hasLoaded) View.GONE else View.VISIBLE 40 | } 41 | } 42 | } 43 | 44 | binding.actionSay.setOnClickListener { 45 | val text = binding.inputTtsText.editText?.text?.toString() 46 | viewModel.say(text) 47 | } 48 | 49 | binding.inputTtsVolume.addOnChangeListener { _, value, fromUser -> 50 | if (fromUser) { 51 | viewModel.setVolume(value) 52 | } 53 | } 54 | 55 | binding.inputTtsVolume.setLabelFormatter { 56 | it.toInt().toString() 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TTS demo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | ⚠️ Please make sure that your runtime environment supports the latest version of Wasm GC and Exception-Handling proposals. 24 | For more information, see https://kotl.in/wasm-help. 25 |
26 |
27 |
    28 |
  • For Chrome and Chromium-based browsers (Edge, Brave etc.), it should just work since version 119.
  • 29 |
  • For Firefox 120 it should just work.
  • 30 |
  • For Firefox 119: 31 |
      32 |
    1. Open about:config in the browser.
    2. 33 |
    3. Enable javascript.options.wasm_gc.
    4. 34 |
    5. Refresh this page.
    6. 35 |
    36 |
  • 37 |
38 |
39 | 40 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/errors/TextToSpeechSynthesisErrors.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts.errors 2 | 3 | private const val defaultErrorMessage = "Error while trying to synthesise text input" 4 | 5 | /** Error that is thrown when synthesising text input fails. */ 6 | sealed class TextToSpeechSynthesisError( 7 | message: String? = defaultErrorMessage, 8 | cause: Throwable? = null 9 | ) : Exception(message, cause) 10 | 11 | /** Error that is thrown when synthesising text input fails because of the user input. */ 12 | class TextToSpeechInputError( 13 | message: String? = defaultErrorMessage, 14 | cause: Throwable? = null 15 | ) : TextToSpeechSynthesisError(message, cause) 16 | 17 | /** Error that is thrown when synthesising text input failed, usually when stop() or close() are called. */ 18 | class TextToSpeechSynthesisInterruptedError( 19 | cause: Throwable? = null 20 | ) : TextToSpeechSynthesisError("TTS synthesis was interrupted by a call to stop() or close()", cause) 21 | 22 | /** Error that is thrown when synthesising text input fails because of the TTS engine. */ 23 | sealed class TextToSpeechEngineError( 24 | message: String? = defaultErrorMessage, 25 | cause: Throwable? = null 26 | ) : TextToSpeechSynthesisError(message, cause) 27 | 28 | /** Error that is thrown when synthesising text input fails. */ 29 | class UnknownTextToSpeechSynthesisError( 30 | cause: Throwable? = null 31 | ) : TextToSpeechEngineError(cause = cause) 32 | 33 | /** Error that is thrown when synthesising text input fails, because the TTS Engine crashed. */ 34 | class TextToSpeechServiceFailureError( 35 | cause: Throwable? = null 36 | ) : TextToSpeechEngineError("The TTS engine crashed while processing the request", cause) 37 | 38 | /** Error that is thrown when synthesising text input fails, because something is wrong with the device audio output. */ 39 | class DeviceAudioOutputError( 40 | cause: Throwable? = null 41 | ) : TextToSpeechEngineError("TTS synthesis unavailable due to device audio output error", cause) 42 | 43 | /** Error that is thrown when synthesising text input fails, because something is wrong with the network. */ 44 | class TextToSpeechNetworkError( 45 | val timeout: Boolean = false, 46 | cause: Throwable? = null 47 | ) : TextToSpeechEngineError("The TTS engine requires network access, but this was not available", cause) 48 | 49 | /** Error that is thrown when synthesising text input fails, because the TTS engine has not been installed (yet). */ 50 | class TextToSpeechEngineUnavailableError( 51 | cause: Throwable? = null 52 | ) : TextToSpeechEngineError("The TTS engine that should handle this request has not been installed (yet)", cause) 53 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/TextToSpeechInstance.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import kotlinx.coroutines.flow.StateFlow 4 | import nl.marc_apps.tts.experimental.ExperimentalTextToSpeechApi 5 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 6 | 7 | /** A TTS instance. Should be [close]d when no longer in use. */ 8 | interface TextToSpeechInstance : AutoCloseable { 9 | val isSynthesizing: StateFlow 10 | 11 | /** 12 | * Value indicating if the engine is warming up. 13 | * Is true after [enqueue] or [say] has been called the first time, 14 | * but before [isSynthesizing] is true. Is false otherwise. 15 | */ 16 | val isWarmingUp: StateFlow 17 | 18 | /** 19 | * The output volume, which is an integer between 0 and 100, set to 100(%) by default. 20 | * Changes only affect new calls to the [say] method. 21 | */ 22 | var volume: Int 23 | 24 | /** 25 | * Alternative to setting [volume] to zero. 26 | * Setting this to true (and back to false) doesn't change the value of [volume]. 27 | * Changes only affect new calls to the [say] method. 28 | */ 29 | var isMuted: Boolean 30 | 31 | var pitch: Float 32 | 33 | var rate: Float 34 | 35 | /** 36 | * Returns a BCP 47 language tag of the selected voice on supported platforms. 37 | * May return the language code as ISO 639 on older platforms. 38 | */ 39 | val language: String 40 | 41 | @ExperimentalVoiceApi 42 | var currentVoice: Voice? 43 | 44 | @ExperimentalVoiceApi 45 | val voices: Sequence 46 | 47 | /** Adds the given [text] to the internal queue, unless [isMuted] is true or [volume] equals 0. */ 48 | fun enqueue(text: String, clearQueue: Boolean = false) 49 | 50 | /** Adds the given [text] to the internal queue, unless [isMuted] is true or [volume] equals 0. */ 51 | fun say(text: String, clearQueue: Boolean = false, callback: (Result) -> Unit) 52 | 53 | /** Adds the given [text] to the internal queue, unless [isMuted] is true or [volume] equals 0. */ 54 | suspend fun say(text: String, clearQueue: Boolean = false, clearQueueOnCancellation: Boolean = false) 55 | 56 | /** Adds the given [text] to the internal queue, unless [isMuted] is true or [volume] equals 0. */ 57 | operator fun plusAssign(text: String) 58 | 59 | /** Clears the internal queue, but doesn't close used resources. */ 60 | fun stop() 61 | 62 | /** Clears the internal queue and closes used resources (if possible) */ 63 | override fun close() 64 | 65 | companion object { 66 | const val VOLUME_MIN = 0 67 | 68 | const val VOLUME_MAX = 100 69 | 70 | const val VOLUME_DEFAULT = VOLUME_MAX 71 | 72 | const val VOICE_PITCH_DEFAULT = 1f 73 | 74 | const val VOICE_RATE_DEFAULT = 1f 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tts/src/commonMain/kotlin/nl/marc_apps/tts/TextToSpeech.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import kotlinx.coroutines.flow.MutableStateFlow 4 | import kotlinx.coroutines.suspendCancellableCoroutine 5 | import nl.marc_apps.tts.utils.CallbackHandler 6 | import nl.marc_apps.tts.utils.ResultHandler 7 | import kotlin.uuid.ExperimentalUuidApi 8 | import kotlin.uuid.Uuid 9 | 10 | @OptIn(ExperimentalUuidApi::class) 11 | internal abstract class TextToSpeech : TextToSpeechInstance { 12 | protected abstract val canDetectSynthesisStarted: Boolean 13 | 14 | protected val callbackHandler = CallbackHandler() 15 | 16 | override val isSynthesizing = MutableStateFlow(false) 17 | 18 | override val isWarmingUp = MutableStateFlow(false) 19 | 20 | private var hasSpoken = false 21 | 22 | protected abstract fun enqueueInternal(text: String, resultHandler: ResultHandler) 23 | 24 | private fun enqueue(text: String, clearQueue: Boolean, resultHandler: ResultHandler){ 25 | if (clearQueue) { 26 | stop() 27 | } 28 | 29 | if (canDetectSynthesisStarted && !hasSpoken) { 30 | hasSpoken = true 31 | isWarmingUp.value = true 32 | } 33 | 34 | if (!canDetectSynthesisStarted) { 35 | isSynthesizing.value = true 36 | } 37 | 38 | enqueueInternal(text, resultHandler) 39 | } 40 | 41 | override fun enqueue(text: String, clearQueue: Boolean) { 42 | enqueue(text, clearQueue, ResultHandler.Empty) 43 | } 44 | 45 | override fun plusAssign(text: String) { 46 | enqueue(text, false, ResultHandler.Empty) 47 | } 48 | 49 | override fun say(text: String, clearQueue: Boolean, callback: (Result) -> Unit) { 50 | enqueue(text, clearQueue, ResultHandler.CallbackHandler(callback)) 51 | } 52 | 53 | override suspend fun say(text: String, clearQueue: Boolean, clearQueueOnCancellation: Boolean){ 54 | suspendCancellableCoroutine { cont -> 55 | enqueue(text, clearQueue, ResultHandler.ContinuationHandler(cont)) 56 | cont.invokeOnCancellation { 57 | if (clearQueueOnCancellation) { 58 | stop() 59 | } 60 | } 61 | } 62 | } 63 | 64 | protected fun onTtsStarted(utteranceId: Uuid) { 65 | isWarmingUp.value = false 66 | isSynthesizing.value = true 67 | } 68 | 69 | protected fun onTtsCompleted(utteranceId: Uuid, result: Result) { 70 | isWarmingUp.value = false 71 | 72 | callbackHandler.onResult(utteranceId, result) 73 | 74 | if (!canDetectSynthesisStarted) { 75 | if (callbackHandler.isQueueEmpty) 76 | { 77 | isSynthesizing.value = false 78 | } 79 | } else { 80 | isSynthesizing.value = false 81 | } 82 | } 83 | 84 | override fun stop() { 85 | callbackHandler.onStopped() 86 | if (!canDetectSynthesisStarted) { 87 | isSynthesizing.value = false 88 | } 89 | } 90 | 91 | override fun close() { 92 | stop() 93 | callbackHandler.clear() 94 | } 95 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /tts/src/appleMain/kotlin/nl/marc_apps/tts/TextToSpeechIOS.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 4 | import nl.marc_apps.tts.utils.ResultHandler 5 | import nl.marc_apps.tts.utils.TtsProgressConverter 6 | import platform.AVFAudio.AVSpeechBoundary 7 | import platform.AVFAudio.AVSpeechSynthesisVoice 8 | import platform.AVFAudio.AVSpeechSynthesizer 9 | import platform.AVFAudio.AVSpeechUtterance 10 | import platform.AVFAudio.AVSpeechUtteranceDefaultSpeechRate 11 | import kotlin.uuid.ExperimentalUuidApi 12 | import kotlin.uuid.Uuid 13 | 14 | @OptIn(ExperimentalUuidApi::class) 15 | internal class TextToSpeechIOS(private val synthesizer: AVSpeechSynthesizer) : TextToSpeech() { 16 | override val canDetectSynthesisStarted = true 17 | 18 | private val internalVolume: Float 19 | get() = if(!isMuted) volume / 100f else 0f 20 | 21 | override var volume: Int = TextToSpeechInstance.VOLUME_DEFAULT 22 | set(value) { 23 | field = when { 24 | value < TextToSpeechInstance.VOLUME_MIN -> TextToSpeechInstance.VOLUME_MIN 25 | value > TextToSpeechInstance.VOLUME_MAX -> TextToSpeechInstance.VOLUME_MAX 26 | else -> value 27 | } 28 | } 29 | 30 | override var isMuted: Boolean = false 31 | 32 | override var pitch: Float = TextToSpeechInstance.VOICE_PITCH_DEFAULT 33 | 34 | override var rate: Float = TextToSpeechInstance.VOICE_RATE_DEFAULT 35 | 36 | override val language: String 37 | get() = AVSpeechSynthesisVoice.currentLanguageCode() 38 | 39 | @ExperimentalVoiceApi 40 | private val defaultVoice: Voice? = AVSpeechUtterance().voice?.let { 41 | IOSVoice(it, true) 42 | } ?: AVSpeechSynthesisVoice.speechVoices().map { it as AVSpeechSynthesisVoice }.firstOrNull { 43 | it.language == AVSpeechSynthesisVoice.currentLanguageCode() 44 | }?.let { IOSVoice(it, true) } 45 | 46 | @ExperimentalVoiceApi 47 | override var currentVoice: Voice? = defaultVoice 48 | 49 | @ExperimentalVoiceApi 50 | override val voices: Sequence = AVSpeechSynthesisVoice.speechVoices().asSequence().map { 51 | IOSVoice(it as AVSpeechSynthesisVoice, it == (defaultVoice as? IOSVoice)?.iosVoice) 52 | } 53 | 54 | private val delegate = TtsProgressConverter(callbackHandler, ::onTtsStarted, ::onTtsCompleted) 55 | 56 | init { 57 | synthesizer.delegate = delegate 58 | } 59 | 60 | @OptIn(ExperimentalVoiceApi::class) 61 | override fun enqueueInternal(text: String, resultHandler: ResultHandler) { 62 | val utterance = AVSpeechUtterance(string = text) 63 | utterance.volume = internalVolume 64 | utterance.rate = rate * AVSpeechUtteranceDefaultSpeechRate 65 | utterance.pitchMultiplier = pitch 66 | val voice = currentVoice 67 | if (voice is IOSVoice) { 68 | utterance.voice = voice.iosVoice 69 | } 70 | 71 | callbackHandler.add(Uuid.random(), utterance, resultHandler) 72 | 73 | synthesizer.speakUtterance(utterance) 74 | } 75 | 76 | override fun stop() { 77 | synthesizer.stopSpeakingAtBoundary(AVSpeechBoundary.AVSpeechBoundaryImmediate) 78 | super.stop() 79 | } 80 | 81 | override fun close() { 82 | super.close() 83 | synthesizer.setDelegate(null) 84 | } 85 | } -------------------------------------------------------------------------------- /demo/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "AppIcon@2x.png", 5 | "idiom": "iphone", 6 | "scale": "2x", 7 | "size": "60x60" 8 | }, 9 | { 10 | "filename": "AppIcon@3x.png", 11 | "idiom": "iphone", 12 | "scale": "3x", 13 | "size": "60x60" 14 | }, 15 | { 16 | "filename": "AppIcon~ipad.png", 17 | "idiom": "ipad", 18 | "scale": "1x", 19 | "size": "76x76" 20 | }, 21 | { 22 | "filename": "AppIcon@2x~ipad.png", 23 | "idiom": "ipad", 24 | "scale": "2x", 25 | "size": "76x76" 26 | }, 27 | { 28 | "filename": "AppIcon-83.5@2x~ipad.png", 29 | "idiom": "ipad", 30 | "scale": "2x", 31 | "size": "83.5x83.5" 32 | }, 33 | { 34 | "filename": "AppIcon-40@2x.png", 35 | "idiom": "iphone", 36 | "scale": "2x", 37 | "size": "40x40" 38 | }, 39 | { 40 | "filename": "AppIcon-40@3x.png", 41 | "idiom": "iphone", 42 | "scale": "3x", 43 | "size": "40x40" 44 | }, 45 | { 46 | "filename": "AppIcon-40~ipad.png", 47 | "idiom": "ipad", 48 | "scale": "1x", 49 | "size": "40x40" 50 | }, 51 | { 52 | "filename": "AppIcon-40@2x~ipad.png", 53 | "idiom": "ipad", 54 | "scale": "2x", 55 | "size": "40x40" 56 | }, 57 | { 58 | "filename": "AppIcon-20@2x.png", 59 | "idiom": "iphone", 60 | "scale": "2x", 61 | "size": "20x20" 62 | }, 63 | { 64 | "filename": "AppIcon-20@3x.png", 65 | "idiom": "iphone", 66 | "scale": "3x", 67 | "size": "20x20" 68 | }, 69 | { 70 | "filename": "AppIcon-20~ipad.png", 71 | "idiom": "ipad", 72 | "scale": "1x", 73 | "size": "20x20" 74 | }, 75 | { 76 | "filename": "AppIcon-20@2x~ipad.png", 77 | "idiom": "ipad", 78 | "scale": "2x", 79 | "size": "20x20" 80 | }, 81 | { 82 | "filename": "AppIcon-29.png", 83 | "idiom": "iphone", 84 | "scale": "1x", 85 | "size": "29x29" 86 | }, 87 | { 88 | "filename": "AppIcon-29@2x.png", 89 | "idiom": "iphone", 90 | "scale": "2x", 91 | "size": "29x29" 92 | }, 93 | { 94 | "filename": "AppIcon-29@3x.png", 95 | "idiom": "iphone", 96 | "scale": "3x", 97 | "size": "29x29" 98 | }, 99 | { 100 | "filename": "AppIcon-29~ipad.png", 101 | "idiom": "ipad", 102 | "scale": "1x", 103 | "size": "29x29" 104 | }, 105 | { 106 | "filename": "AppIcon-29@2x~ipad.png", 107 | "idiom": "ipad", 108 | "scale": "2x", 109 | "size": "29x29" 110 | }, 111 | { 112 | "filename": "AppIcon-60@2x~car.png", 113 | "idiom": "car", 114 | "scale": "2x", 115 | "size": "60x60" 116 | }, 117 | { 118 | "filename": "AppIcon-60@3x~car.png", 119 | "idiom": "car", 120 | "scale": "3x", 121 | "size": "60x60" 122 | }, 123 | { 124 | "filename": "AppIcon~ios-marketing.png", 125 | "idiom": "ios-marketing", 126 | "scale": "1x", 127 | "size": "1024x1024" 128 | } 129 | ], 130 | "info": { 131 | "author": "iconkitchen", 132 | "version": 1 133 | } 134 | } -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | android-buildTools = "35.0.0" 3 | android-compileSdk = "35" 4 | android-gradlePlugin = "8.7.3" 5 | 6 | androidx-annotation = "1.9.1" 7 | androidx-appcompat = "1.7.0" 8 | androidx-constraintlayout = "2.2.0" 9 | androidx-fragment = "1.8.5" 10 | androidx-lifecycle-runtime = "2.8.7" 11 | 12 | compose = "1.7.3" 13 | 14 | dokka = "2.0.0" 15 | 16 | freetts_java = "1.2.2" 17 | 18 | google-material-xml = "1.12.0" 19 | 20 | koin = "4.0.0" 21 | koin-annotations = "2.0.0-Beta2" 22 | 23 | kotlin = "2.0.21" 24 | kotlin-coroutines = "1.9.0" 25 | kotlin-browser = "0.3" 26 | 27 | ksp = "2.0.21-1.0.28" 28 | 29 | tts = "2.5.0" 30 | 31 | versioncheck = "0.51.0" 32 | 33 | mavenPublishPlugin = "0.32.0" 34 | 35 | [libraries] 36 | androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } 37 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } 38 | androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" } 39 | androidx-fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" } 40 | androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-runtime" } 41 | 42 | dokka-plugins-androidDocs = { module = "org.jetbrains.dokka:android-documentation-plugin", version.ref = "dokka" } 43 | dokka-plugins-versioning = { module = "org.jetbrains.dokka:versioning-plugin", version.ref = "dokka" } 44 | 45 | freetts = { module = "net.sf.sociaal:freetts", version.ref = "freetts_java" } 46 | 47 | google-material-xml = { module = "com.google.android.material:material", version.ref = "google-material-xml" } 48 | 49 | koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin" } 50 | koin-android = { module = "io.insert-koin:koin-android" } 51 | koin-androidx-startup = { module = "io.insert-koin:koin-androidx-startup" } 52 | koin-annotations-bom = { module = "io.insert-koin:koin-annotations-bom", version.ref = "koin-annotations" } 53 | koin-annotations = { module = "io.insert-koin:koin-annotations" } 54 | koin-annotations-ksp = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koin-annotations" } 55 | 56 | kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" } 57 | kotlin-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlin-browser" } 58 | 59 | [plugins] 60 | android-application = { id = "com.android.application", version.ref = "android-gradlePlugin" } 61 | android-library = { id = "com.android.library", version.ref = "android-gradlePlugin" } 62 | 63 | compose = { id = "org.jetbrains.compose", version.ref = "compose" } 64 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 65 | 66 | dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } 67 | 68 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 69 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 70 | 71 | ksp = { id = "com.google.devtools.ksp", version.ref="ksp" } 72 | 73 | mavenPublishPlugin = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublishPlugin" } 74 | 75 | versioncheck = { id = "com.github.ben-manes.versions", version.ref = "versioncheck" } 76 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # region Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | 8 | # Specifies the JVM arguments used for the daemon process. 9 | # The setting is particularly useful for tweaking memory settings. 10 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 11 | 12 | # When configured, Gradle will run in incubating parallel mode. 13 | # This option should only be used with decoupled projects. More details, visit 14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 15 | # org.gradle.parallel=true 16 | 17 | # Enable Gradle build cache 18 | org.gradle.caching=true 19 | 20 | # Enables new incubating mode that makes Gradle selective when configuring projects. 21 | # Only relevant projects are configured which results in faster builds for large multi-projects. 22 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:configuration_on_demand 23 | org.gradle.configureondemand=true 24 | # endregion 25 | 26 | # region Android settings 27 | # AndroidX package structure to make it clearer which packages are bundled with the 28 | # Android operating system, and which are packaged with your app's APK 29 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 30 | android.useAndroidX=true 31 | 32 | # Automatically convert third-party libraries to use AndroidX 33 | # android.enableJetifier=true 34 | 35 | # Enables namespacing of each library's R class so that its R class includes only the 36 | # resources declared in the library itself and none from the library's dependencies, 37 | # thereby reducing the size of the R class for that library 38 | android.nonTransitiveRClass=true 39 | 40 | # Enable aggressive optimizations for R8 41 | android.enableR8.fullMode=true 42 | 43 | # Don't generate BuildConfig 44 | android.defaults.buildfeatures.buildconfig=false 45 | 46 | # Make resource IDs non-final 47 | android.nonFinalResIds=true 48 | # endregion 49 | 50 | # region Kotlin settings 51 | # Kotlin code style for this project: "official" or "obsolete": 52 | kotlin.code.style=official 53 | 54 | # Kotlin Multiplatform 55 | kotlin.mpp.androidSourceSetLayoutVersion=2 56 | 57 | # Compose Multiplatform 58 | org.jetbrains.compose.experimental.jscanvas.enabled=true 59 | org.jetbrains.compose.experimental.wasm.enabled=true 60 | org.jetbrains.compose.experimental.macos.enabled=true 61 | 62 | # Dokka 63 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 64 | # endregion 65 | 66 | # region Publishing POM 67 | POM_NAME=TextToSpeechKt 68 | POM_DESCRIPTION=Text-to-Speech library for Kotlin Multiplatform. Supports targets Android, iOS, macOS and browser (js and wasmJs). 69 | POM_INCEPTION_YEAR=2020 70 | POM_URL=https://github.com/Marc-JB/TextToSpeechKt 71 | 72 | POM_LICENSE_NAME=The MIT License (MIT) 73 | POM_LICENSE_URL=https://opensource.org/licenses/MIT 74 | POM_LICENSE_DIST=repo 75 | 76 | POM_SCM_URL=https://github.com/Marc-JB/TextToSpeechKt/ 77 | POM_SCM_CONNECTION=scm:git:git://github.com/Marc-JB/TextToSpeechKt.git 78 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/Marc-JB/TextToSpeechKt.git 79 | 80 | POM_DEVELOPER_ID=Marc-JB 81 | POM_DEVELOPER_NAME=Marc 82 | POM_DEVELOPER_URL=https://marc-apps.nl 83 | # endregion 84 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/commonMain/kotlin/nl/marc_apps/tts_demo/TtsDemoView.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.rememberScrollState 5 | import androidx.compose.foundation.verticalScroll 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.rounded.GraphicEq 8 | import androidx.compose.material.icons.rounded.RecordVoiceOver 9 | import androidx.compose.material3.* 10 | import androidx.compose.runtime.* 11 | import androidx.compose.runtime.saveable.rememberSaveable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.platform.LocalFocusManager 15 | import androidx.compose.ui.unit.dp 16 | import kotlinx.coroutines.launch 17 | import nl.marc_apps.tts.TextToSpeechInstance 18 | import org.jetbrains.compose.resources.stringResource 19 | import nl.marc_apps.tts_demo.resources.* 20 | 21 | @Composable 22 | fun TtsDemoView( 23 | textToSpeech: TextToSpeechInstance?, 24 | paddingValues: PaddingValues 25 | ) { 26 | val coroutineScope = rememberCoroutineScope() 27 | val scrollState = rememberScrollState() 28 | 29 | Surface( 30 | color = MaterialTheme.colorScheme.background, 31 | modifier = Modifier 32 | .fillMaxSize() 33 | .imePadding() 34 | .verticalScroll(scrollState) 35 | .padding(paddingValues) 36 | .padding(24.dp) 37 | ) { 38 | Column( 39 | verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), 40 | horizontalAlignment = Alignment.Start 41 | ) { 42 | if (textToSpeech == null) { 43 | Text(stringResource(Res.string.tts_not_available)) 44 | } else { 45 | val focusManager = LocalFocusManager.current 46 | 47 | var text by rememberSaveable { mutableStateOf("") } 48 | 49 | val isSynthesizing by textToSpeech.isSynthesizing.collectAsState() 50 | 51 | Row ( 52 | horizontalArrangement = Arrangement.spacedBy(16.dp) 53 | ) { 54 | Icon(Icons.Rounded.GraphicEq, contentDescription = null) 55 | 56 | Text(stringResource(if (isSynthesizing) Res.string.synthesizing_status_active else Res.string.synthesizing_status_inactive)) 57 | } 58 | 59 | OutlinedTextField( 60 | value = text, 61 | onValueChange = { text = it }, 62 | label = { Text(stringResource(Res.string.tts_input_label)) }, 63 | modifier = Modifier.fillMaxWidth() 64 | ) 65 | 66 | OptionsCard(textToSpeech) 67 | 68 | Spacer(Modifier.weight(1f)) 69 | 70 | ElevatedButton( 71 | onClick = { 72 | focusManager.clearFocus() 73 | coroutineScope.launch { 74 | textToSpeech.say(text) 75 | } 76 | }, 77 | enabled = text.isNotBlank(), 78 | modifier = Modifier.align(Alignment.End), 79 | contentPadding = PaddingValues(32.dp, 16.dp) 80 | ) { 81 | Icon(Icons.Rounded.RecordVoiceOver, contentDescription = null) 82 | 83 | Spacer(Modifier.width(16.dp)) 84 | 85 | Text(stringResource(Res.string.action_say)) 86 | } 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tts/src/jvmMain/kotlin/nl/marc_apps/tts/TextToSpeechDesktop.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import com.sun.speech.freetts.VoiceManager 4 | import kotlinx.coroutines.SupervisorJob 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.launch 7 | import kotlinx.coroutines.suspendCancellableCoroutine 8 | import nl.marc_apps.tts.experimental.ExperimentalDesktopTarget 9 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 10 | import nl.marc_apps.tts.utils.CallbackHandler 11 | import nl.marc_apps.tts.utils.ResultHandler 12 | import nl.marc_apps.tts.utils.SynthesisScope 13 | import kotlin.uuid.ExperimentalUuidApi 14 | import kotlin.uuid.Uuid 15 | 16 | private const val VOICE_NAME = "kevin16" 17 | 18 | @OptIn(ExperimentalUuidApi::class) 19 | @ExperimentalDesktopTarget 20 | internal class TextToSpeechDesktop(voiceManager: VoiceManager) : TextToSpeech() { 21 | override val canDetectSynthesisStarted = false 22 | 23 | private val supervisor = SupervisorJob() 24 | private val synthesisScope = SynthesisScope(supervisor) 25 | 26 | private val voice = voiceManager.getVoice(VOICE_NAME) 27 | 28 | override val language = voice.locale.toLanguageTag() 29 | 30 | override var volume = 100 31 | set(value) { 32 | if (value in 0..100) { 33 | voice.volume = if(isMuted) 0f else value / 100f 34 | field = value 35 | } 36 | } 37 | 38 | override var isMuted = false 39 | set(value) { 40 | voice.volume = if(value) 0f else volume / 100f 41 | field = value 42 | } 43 | 44 | private val defaultPitch = voice.pitch 45 | 46 | override var pitch = 1f 47 | set(value) { 48 | voice.pitch = value * defaultPitch 49 | field = value 50 | } 51 | 52 | private val defaultRate = voice.rate 53 | 54 | override var rate = 1f 55 | set(value) { 56 | voice.rate = value * defaultRate 57 | field = value 58 | } 59 | 60 | @ExperimentalVoiceApi 61 | private val defaultVoice = object : Voice { 62 | override val name = "Kevin" 63 | 64 | override val isDefault = true 65 | 66 | override val isOnline = false 67 | 68 | override val languageTag = voice.locale.toLanguageTag() 69 | 70 | override val language = voice.locale.displayLanguage 71 | 72 | override val region = voice.locale.displayCountry 73 | 74 | override val locale = voice.locale 75 | } 76 | 77 | @ExperimentalVoiceApi 78 | @Suppress("SetterBackingFieldAssignment") 79 | override var currentVoice: Voice? = defaultVoice 80 | set(_) { 81 | // Ignored: Not supported yet 82 | } 83 | 84 | @ExperimentalVoiceApi 85 | override val voices: Sequence = sequence { 86 | yield(defaultVoice) 87 | } 88 | 89 | init { 90 | voice.allocate() 91 | isWarmingUp.value = false 92 | } 93 | 94 | override fun enqueueInternal(text: String, resultHandler: ResultHandler) { 95 | val utteranceId = Uuid.random() 96 | 97 | callbackHandler.add(utteranceId, null, resultHandler) 98 | 99 | synthesisScope.launch { 100 | voice.speak(text) 101 | onTtsCompleted(utteranceId, Result.success(Unit)) 102 | } 103 | } 104 | 105 | override fun stop() { 106 | voice.outputQueue.removeAll() 107 | super.stop() 108 | } 109 | 110 | override fun close() { 111 | super.close() 112 | voice.deallocate() 113 | supervisor.cancel() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Run_Android_XML_Demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 70 | -------------------------------------------------------------------------------- /tts/src/androidMain/kotlin/nl/marc_apps/tts/TextToSpeechFactory.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.speech.tts.TextToSpeech 6 | import androidx.annotation.ChecksSdkIntAtLeast 7 | import nl.marc_apps.tts.errors.TextToSpeechNotSupportedError 8 | import nl.marc_apps.tts.errors.UnknownTextToSpeechInitialisationError 9 | import nl.marc_apps.tts.utils.ErrorCodes 10 | import kotlin.coroutines.resume 11 | import kotlin.coroutines.resumeWithException 12 | import kotlin.coroutines.suspendCoroutine 13 | 14 | /** 15 | * Factory class to create a Text-to-Speech instance. 16 | * Use [defaultEngine] to set the package name of the default engine. 17 | * Setting this to [TextToSpeechEngine.Google] is recommended. 18 | */ 19 | actual class TextToSpeechFactory( 20 | private val context: Context, 21 | private val defaultEngine: TextToSpeechEngine = TextToSpeechEngine.SystemDefault 22 | ) { 23 | /** 24 | * Factory class to create a Text-to-Speech instance. 25 | * Use [defaultSpeechEngine] to set the package name of the default engine. 26 | * Setting this to [TextToSpeechEngine.Google.androidPackage] is recommended. 27 | */ 28 | @Deprecated("Replaced by constructor with TextToSpeechEngine class") 29 | constructor(context: Context, defaultSpeechEngine: String? = null) : this(context, 30 | defaultSpeechEngine?.let { TextToSpeechEngine.Custom(it) } ?: TextToSpeechEngine.SystemDefault) 31 | 32 | @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.DONUT) 33 | actual val isSupported: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.DONUT 34 | 35 | @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.HONEYCOMB) 36 | actual val canChangeVolume = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB 37 | 38 | actual suspend fun create(): Result { 39 | return runCatching { createOrThrow() } 40 | } 41 | 42 | @Throws(RuntimeException::class) 43 | actual suspend fun createOrThrow(): TextToSpeechInstance { 44 | return suspendCoroutine { continuation -> 45 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.DONUT) { 46 | lateinit var obj: TextToSpeech 47 | 48 | try { 49 | val initListener = TextToSpeech.OnInitListener { responseCode -> 50 | if (responseCode == TextToSpeech.SUCCESS) { 51 | continuation.resume(TextToSpeechAndroid(obj)) 52 | } else { 53 | continuation.resumeWithException(ErrorCodes.mapToThrowable(responseCode)) 54 | } 55 | } 56 | 57 | obj = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 58 | TextToSpeech(context, initListener, defaultEngine.androidPackage) 59 | } else { 60 | TextToSpeech(context, initListener) 61 | } 62 | } catch (e: Throwable) { 63 | continuation.resumeWithException(UnknownTextToSpeechInitialisationError(e)) 64 | } 65 | } else continuation.resumeWithException(TextToSpeechNotSupportedError()) 66 | } 67 | } 68 | 69 | actual suspend fun createOrNull(): TextToSpeechInstance? { 70 | return create().getOrNull() 71 | } 72 | 73 | companion object { 74 | @Deprecated("Replaced by TextToSpeechEngine.Google.androidPackage", ReplaceWith("TextToSpeechEngine.Google.androidPackage")) 75 | val ENGINE_SPEECH_SERVICES_BY_GOOGLE = TextToSpeechEngine.Google.androidPackage 76 | 77 | @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.DONUT) 78 | val isSupported: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.DONUT 79 | 80 | @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.HONEYCOMB) 81 | val canChangeVolume = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tts-compose/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import com.vanniktech.maven.publish.JavadocJar 4 | import com.vanniktech.maven.publish.KotlinMultiplatform 5 | import com.vanniktech.maven.publish.SonatypeHost 6 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 7 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 8 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 9 | 10 | plugins { 11 | alias(libs.plugins.kotlin.multiplatform) 12 | alias(libs.plugins.android.library) 13 | alias(libs.plugins.compose) 14 | alias(libs.plugins.compose.compiler) 15 | alias(libs.plugins.dokka) 16 | alias(libs.plugins.mavenPublishPlugin) 17 | } 18 | 19 | object Project { 20 | const val ARTIFACT_ID = "tts-compose" 21 | const val NAMESPACE = "nl.marc_apps.tts_compose" 22 | } 23 | 24 | group = "nl.marc-apps" 25 | version = libs.versions.tts.get() 26 | 27 | kotlin { 28 | js { 29 | browser() 30 | binaries.executable() 31 | } 32 | 33 | @OptIn(ExperimentalWasmDsl::class) 34 | wasmJs { 35 | browser() 36 | binaries.executable() 37 | } 38 | 39 | androidTarget { 40 | publishLibraryVariants("release") 41 | 42 | compilerOptions { 43 | jvmTarget = JvmTarget.JVM_1_8 44 | } 45 | } 46 | 47 | iosX64() 48 | iosArm64() 49 | iosSimulatorArm64() 50 | macosArm64() 51 | macosX64() 52 | 53 | jvm { 54 | compilerOptions { 55 | jvmTarget = JvmTarget.JVM_17 56 | } 57 | } 58 | 59 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 60 | applyDefaultHierarchyTemplate { 61 | common { 62 | group("webCommonW3C") { 63 | withJs() 64 | withWasmJs() 65 | } 66 | } 67 | } 68 | 69 | sourceSets { 70 | commonMain.dependencies { 71 | implementation(compose.runtime) 72 | implementation(libs.kotlin.coroutines) 73 | api(projects.tts) 74 | } 75 | 76 | androidMain.dependencies { 77 | implementation(compose.foundation) 78 | } 79 | 80 | wasmJsMain.dependencies { 81 | implementation(libs.kotlin.browser) 82 | } 83 | } 84 | } 85 | 86 | android { 87 | compileSdk = libs.versions.android.compileSdk.get().toInt() 88 | buildToolsVersion = libs.versions.android.buildTools.get() 89 | 90 | namespace = Project.NAMESPACE 91 | 92 | defaultConfig { 93 | minSdk = 21 94 | 95 | setProperty("archivesBaseName", Project.ARTIFACT_ID) 96 | } 97 | } 98 | 99 | dokka { 100 | dokkaSourceSets.configureEach { 101 | sourceLink { 102 | localDirectory = file("src/${name}/kotlin") 103 | remoteUrl("https://github.com/Marc-JB/TextToSpeechKt/blob/main/${Project.ARTIFACT_ID}/src/${name}/kotlin") 104 | remoteLineSuffix = "#L" 105 | } 106 | 107 | externalDocumentationLinks { 108 | create("tts") { 109 | url("https://marc-jb.github.io/TextToSpeechKt") 110 | packageListUrl("https://marc-jb.github.io/TextToSpeechKt/package-list") 111 | } 112 | } 113 | 114 | if (name.startsWith("android")){ 115 | jdkVersion.set(JavaVersion.VERSION_1_8.majorVersion.toInt()) 116 | } else if (name.startsWith("jvm")){ 117 | jdkVersion.set(JavaVersion.VERSION_17.majorVersion.toInt()) 118 | } 119 | } 120 | } 121 | 122 | mavenPublishing { 123 | coordinates("nl.marc-apps", Project.ARTIFACT_ID, libs.versions.tts.get()) 124 | 125 | configure(KotlinMultiplatform( 126 | javadocJar = JavadocJar.Dokka("dokkaGeneratePublicationHtml") 127 | )) 128 | 129 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 130 | 131 | /*repositories { 132 | maven { 133 | name = "githubPackages" 134 | url = uri("https://maven.pkg.github.com/Marc-JB/TextToSpeechKt") 135 | credentials(PasswordCredentials::class) 136 | } 137 | }*/ 138 | 139 | signAllPublications() 140 | } 141 | -------------------------------------------------------------------------------- /tts/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import com.vanniktech.maven.publish.JavadocJar 4 | import com.vanniktech.maven.publish.KotlinMultiplatform 5 | import com.vanniktech.maven.publish.SonatypeHost 6 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 7 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 8 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 9 | 10 | plugins { 11 | alias(libs.plugins.kotlin.multiplatform) 12 | alias(libs.plugins.android.library) 13 | alias(libs.plugins.dokka) 14 | alias(libs.plugins.mavenPublishPlugin) 15 | } 16 | 17 | object Project { 18 | const val ARTIFACT_ID = "tts" 19 | const val NAMESPACE = "nl.marc_apps.tts" 20 | } 21 | 22 | group = "nl.marc-apps" 23 | version = libs.versions.tts.get() 24 | 25 | kotlin { 26 | js { 27 | browser() 28 | binaries.executable() 29 | } 30 | 31 | @OptIn(ExperimentalWasmDsl::class) 32 | wasmJs { 33 | browser() 34 | binaries.executable() 35 | } 36 | 37 | androidTarget { 38 | publishLibraryVariants("release") 39 | 40 | compilerOptions { 41 | jvmTarget = JvmTarget.JVM_1_8 42 | } 43 | } 44 | 45 | iosX64() 46 | iosArm64() 47 | iosSimulatorArm64() 48 | macosArm64() 49 | macosX64() 50 | 51 | jvm { 52 | compilerOptions { 53 | jvmTarget = JvmTarget.JVM_17 54 | } 55 | } 56 | 57 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 58 | applyDefaultHierarchyTemplate { 59 | common { 60 | group("webCommonW3C") { 61 | withJs() 62 | withWasmJs() 63 | } 64 | } 65 | } 66 | 67 | compilerOptions { 68 | freeCompilerArgs.add("-Xexpect-actual-classes") 69 | } 70 | 71 | sourceSets { 72 | commonMain.dependencies { 73 | implementation(libs.kotlin.coroutines) 74 | } 75 | 76 | androidMain.dependencies { 77 | implementation(libs.androidx.annotation) 78 | } 79 | 80 | jvmMain.dependencies { 81 | implementation(libs.freetts) 82 | } 83 | 84 | wasmJsMain.dependencies { 85 | implementation(libs.kotlin.browser) 86 | } 87 | } 88 | } 89 | 90 | android { 91 | compileSdk = libs.versions.android.compileSdk.get().toInt() 92 | buildToolsVersion = libs.versions.android.buildTools.get() 93 | 94 | namespace = Project.NAMESPACE 95 | 96 | defaultConfig { 97 | minSdk = 1 98 | 99 | setProperty("archivesBaseName", Project.ARTIFACT_ID) 100 | } 101 | } 102 | 103 | dokka { 104 | dokkaSourceSets.configureEach { 105 | sourceLink { 106 | localDirectory = file("src/${name}/kotlin") 107 | remoteUrl("https://github.com/Marc-JB/TextToSpeechKt/blob/main/${Project.ARTIFACT_ID}/src/${name}/kotlin") 108 | remoteLineSuffix = "#L" 109 | } 110 | 111 | externalDocumentationLinks { 112 | create("tts") { 113 | url("https://marc-jb.github.io/TextToSpeechKt") 114 | packageListUrl("https://marc-jb.github.io/TextToSpeechKt/package-list") 115 | } 116 | } 117 | 118 | if (name.startsWith("android")){ 119 | jdkVersion.set(JavaVersion.VERSION_1_8.majorVersion.toInt()) 120 | } else if (name.startsWith("jvm")){ 121 | jdkVersion.set(JavaVersion.VERSION_17.majorVersion.toInt()) 122 | } 123 | } 124 | } 125 | 126 | mavenPublishing { 127 | coordinates("nl.marc-apps", Project.ARTIFACT_ID, libs.versions.tts.get()) 128 | 129 | configure(KotlinMultiplatform( 130 | javadocJar = JavadocJar.Dokka("dokkaGeneratePublicationHtml") 131 | )) 132 | 133 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 134 | 135 | /*repositories { 136 | maven { 137 | name = "githubPackages" 138 | url = uri("https://maven.pkg.github.com/Marc-JB/TextToSpeechKt") 139 | credentials(PasswordCredentials::class) 140 | } 141 | }*/ 142 | 143 | signAllPublications() 144 | } 145 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 122 | 123 | -------------------------------------------------------------------------------- /tts/src/webCommonW3CMain/kotlin/nl/marc_apps/tts/TextToSpeechBrowser.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package nl.marc_apps.tts 4 | 5 | import js_interop.Window 6 | import js_interop.getSpeechSynthesis 7 | import js_interop.getVoiceList 8 | import js_interop.window 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.suspendCancellableCoroutine 11 | import nl.marc_apps.tts.errors.UnknownTextToSpeechSynthesisError 12 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 13 | import nl.marc_apps.tts.utils.CallbackHandler 14 | import nl.marc_apps.tts.utils.ResultHandler 15 | import org.w3c.speech.SpeechSynthesis 16 | import org.w3c.speech.SpeechSynthesisUtterance 17 | import kotlin.coroutines.resume 18 | import kotlin.coroutines.resumeWithException 19 | import kotlin.uuid.ExperimentalUuidApi 20 | import kotlin.uuid.Uuid 21 | 22 | @OptIn(ExperimentalUuidApi::class) 23 | internal class TextToSpeechBrowser(context: Window = window) : TextToSpeech() { 24 | override val canDetectSynthesisStarted = true 25 | 26 | private val speechSynthesis: SpeechSynthesis = getSpeechSynthesis(context) 27 | 28 | private var speechSynthesisUtterance = SpeechSynthesisUtterance() 29 | 30 | private val internalVolume: Float 31 | get() = if(!isMuted) volume / 100f else 0f 32 | 33 | override var volume: Int = TextToSpeechInstance.VOLUME_DEFAULT 34 | set(value) { 35 | field = when { 36 | value < TextToSpeechInstance.VOLUME_MIN -> TextToSpeechInstance.VOLUME_MIN 37 | value > TextToSpeechInstance.VOLUME_MAX -> TextToSpeechInstance.VOLUME_MAX 38 | else -> value 39 | } 40 | speechSynthesisUtterance.volume = internalVolume 41 | } 42 | 43 | override var isMuted = false 44 | set(value) { 45 | field = value 46 | speechSynthesisUtterance.volume = internalVolume 47 | } 48 | 49 | override var pitch = TextToSpeechInstance.VOICE_PITCH_DEFAULT 50 | set(value) { 51 | field = value 52 | speechSynthesisUtterance.pitch = value 53 | } 54 | 55 | override var rate = TextToSpeechInstance.VOICE_RATE_DEFAULT 56 | set(value) { 57 | field = value 58 | speechSynthesisUtterance.rate = value 59 | } 60 | 61 | private val voiceList by lazy { 62 | getVoiceList(speechSynthesis) 63 | } 64 | 65 | override val language: String 66 | get() { 67 | val reportedLanguage = speechSynthesisUtterance.voice?.lang ?: speechSynthesisUtterance.lang 68 | return reportedLanguage.ifBlank { 69 | val defaultLanguage = voiceList.find { it.default }?.lang 70 | if (defaultLanguage.isNullOrBlank()) "Unknown" else defaultLanguage 71 | } 72 | } 73 | 74 | @ExperimentalVoiceApi 75 | private val defaultVoice by lazy { 76 | voiceList.find { it.default }?.let { BrowserVoice(it) } 77 | } 78 | 79 | @ExperimentalVoiceApi 80 | override var currentVoice: Voice? = null 81 | get() = field ?: defaultVoice 82 | set(value) { 83 | if (value is BrowserVoice) { 84 | speechSynthesisUtterance.voice = value.browserVoice 85 | field = value 86 | } 87 | } 88 | 89 | @ExperimentalVoiceApi 90 | override val voices: Sequence by lazy { 91 | voiceList.asSequence().map { BrowserVoice(it) } 92 | } 93 | 94 | @OptIn(ExperimentalVoiceApi::class) 95 | private fun resetCurrentUtterance() { 96 | speechSynthesisUtterance = SpeechSynthesisUtterance().also { 97 | it.volume = internalVolume 98 | it.pitch = pitch 99 | it.rate = rate 100 | it.voice = (currentVoice as? BrowserVoice)?.browserVoice 101 | } 102 | } 103 | 104 | override fun enqueueInternal(text: String, resultHandler: ResultHandler) { 105 | val utteranceId = Uuid.random() 106 | 107 | callbackHandler.add(utteranceId, null, resultHandler) 108 | 109 | speechSynthesisUtterance.onstart = { onTtsStarted(utteranceId) } 110 | speechSynthesisUtterance.onend = { onTtsCompleted(utteranceId, Result.success(Unit)) } 111 | 112 | speechSynthesisUtterance.text = text 113 | speechSynthesis.speak(speechSynthesisUtterance) 114 | 115 | resetCurrentUtterance() 116 | } 117 | 118 | override fun stop() { 119 | speechSynthesis.cancel() 120 | super.stop() 121 | } 122 | 123 | override fun close() { 124 | super.close() 125 | callbackHandler.clear() 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # TextToSpeechKt 4 | Text-to-Speech library for Kotlin Multiplatform. Supports targets Android, iOS, macOS and browser (js and wasmJs). 5 | 6 | [![Gradle deployment](https://github.com/Marc-JB/TextToSpeechKt/actions/workflows/deployment.yml/badge.svg)](https://github.com/Marc-JB/TextToSpeechKt/actions) 7 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Marc-JB_TextToSpeechKt&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Marc-JB_TextToSpeechKt) 8 | [![Maven Central](https://badgen.net/maven/v/maven-central/nl.marc-apps/tts)](https://central.sonatype.com/search?smo=true&namespace=nl.marc-apps&q=tts) 9 | [![License](https://badgen.net/github/license/Marc-JB/TextToSpeechKt)](https://github.com/Marc-JB/TextToSpeechKt/blob/main/LICENSE) 10 | 11 |
12 | 13 | # :notebook_with_decorative_cover: Table of Contents 14 | - [About the Project](#star2-about-the-project) 15 | * [Tech Stack](#space_invader-tech-stack) 16 | * [Features](#dart-features) 17 | - [Getting Started](#toolbox-getting-started) 18 | * [Prerequisites](#bangbang-prerequisites) 19 | * [Installation](#gear-installation) 20 | - [Usage](#eyes-usage) 21 | - [License](#warning-license) 22 | - [Acknowledgements](#gem-acknowledgements) 23 | 24 | ## :star2: About the Project 25 | ### :space_invader: Tech Stack 26 | Uses Kotlin Multiplatform with support for the following targets: 27 | 28 | | Platform | Language | Support | 29 | |---------------------|---------------|---------------------------| 30 | | Android | Kotlin/JVM | ✅ | 31 | | Browser | Kotlin/JS | ✅ | 32 | | Browser | Kotlin/Wasm | ✅ | 33 | | Browser | JS, TS | ❌ _Support ended in v2.0_ | 34 | | Desktop | Kotlin/JVM | ⚠️ _Experimental support_ | 35 | | iOS, MacOS | Kotlin/Native | ✅ | 36 | | Other Kotlin/Native | Kotlin/Native | ❌ | 37 | 38 | 39 | ### :dart: Features 40 | - Kotlin Multiplatform API for text-to-speech on the following platforms: Android, iOS, macOS, js and wasmJs 41 | - Await synthesis completion using Kotlin Coroutines 42 | - Supports the following configuration: 43 | * Voice pitch & rate 44 | * Volume 45 | * Voice selection 46 | * Language (through voice selection) 47 | - Compose Multiplatform support with `rememberTextToSpeechOrNull()` available in the `tts-compose` package. 48 | 49 | ## :toolbox: Getting Started 50 | ### :bangbang: Prerequisites 51 | A build tool like Gradle or Maven. 52 | 53 | ### :gear: Installation 54 |
55 | libs.versions.toml 56 | 57 | ```Toml copy 58 | [versions] 59 | textToSpeech = "3.0.0" 60 | 61 | [libraries] 62 | textToSpeech = { module = "nl.marc-apps:tts", version.ref = "textToSpeech" } 63 | # Optional: Extensions for Compose 64 | textToSpeech-compose = { module = "nl.marc-apps:tts-compose", version.ref = "textToSpeech" } 65 | ``` 66 | 67 | Make sure to configure the latest stable version: [![Maven Central](https://badgen.net/maven/v/maven-central/nl.marc-apps/tts)](https://central.sonatype.com/search?smo=true&namespace=nl.marc-apps&q=tts) 68 | 69 |
70 | 71 |
72 | Gradle 73 | 74 | And add the library to your dependencies: 75 | ```Kotlin copy 76 | dependencies { 77 | implementation("nl.marc-apps:tts:3.0.0") 78 | 79 | // Optional: Extensions for Compose 80 | implementation("nl.marc-apps:tts-compose:3.0.0") 81 | } 82 | ``` 83 | 84 | Or 85 | 86 | ```Kotlin copy 87 | kotlin { 88 | sourceSets { 89 | commonMain.dependencies { 90 | implementation("nl.marc-apps:tts:3.0.0") 91 | 92 | // Optional: Extensions for Compose 93 | implementation("nl.marc-apps:tts-compose:3.0.0") 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | Make sure to configure the latest stable version: [![Maven Central](https://badgen.net/maven/v/maven-central/nl.marc-apps/tts)](https://central.sonatype.com/search?smo=true&namespace=nl.marc-apps&q=tts) 100 | 101 |
102 | 103 | ## :eyes: Usage 104 | ### Documentation files 105 | [View documentation generated by Dokka](https://marc-jb.github.io/TextToSpeechKt/index.html) 106 | 107 | ### Demo projects 108 | Go to the [/demo directory](/demo) of this project. 109 | 110 | ## :warning: License 111 | This project is published under the MIT License. Read more about this license in the `LICENSE` file. 112 | 113 | ## :gem: Acknowledgements 114 | - [Awesome Readme Template](https://github.com/Louis3797/awesome-readme-template) 115 | - [Badgen](https://badgen.net/) 116 | -------------------------------------------------------------------------------- /.github/workflows/deployment.yml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version number for the release' 8 | required: true 9 | type: 'string' 10 | create_tag: 11 | description: 'Create a tag' 12 | required: true 13 | default: true 14 | type: 'boolean' 15 | 16 | concurrency: 17 | group: deployment-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | permissions: 21 | contents: write 22 | packages: write 23 | 24 | jobs: 25 | build-and-deploy-lib: 26 | runs-on: macos-latest 27 | timeout-minutes: 14 28 | steps: 29 | 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | 33 | - name: Set up JDK 17 34 | uses: actions/setup-java@v4 35 | with: 36 | java-version: 17 37 | distribution: 'temurin' 38 | cache: 'gradle' 39 | 40 | - name: Grant execute permission for gradlew 41 | run: chmod +x gradlew 42 | 43 | - name: Update version 44 | run: sed -i '' 's/tts[[:space:]]*=[[:space:]]*"[^"]*"/tts = "${{ github.event.inputs.version }}"/g' ./gradle/libs.versions.toml 45 | 46 | - name: Build & Deploy with Gradle to Maven 47 | run: ./gradlew :tts:publishAndReleaseToMavenCentral :tts-compose:publishAndReleaseToMavenCentral 48 | env: 49 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 50 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_TOKEN }} 51 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SIGNING_KEY }} 52 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SIGNING_PASSWORD }} 53 | 54 | # - name: Build & Deploy with Gradle to GitHub Packages 55 | # continue-on-error: true 56 | # run: ./gradlew :tts:publishAllPublicationsToGithubPackagesRepository :tts-compose:publishAllPublicationsToGithubPackagesRepository 57 | # env: 58 | # ORG_GRADLE_PROJECT_githubPackagesUsername: github-actions 59 | # ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }} 60 | # ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SIGNING_KEY }} 61 | # ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SIGNING_PASSWORD }} 62 | 63 | - name: Upload library artifacts 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: TextToSpeechKt.Libs 67 | path: | 68 | ./tts/build/outputs/aar/tts-*.aar 69 | ./tts/build/libs/tts-*.klib 70 | ./tts/build/libs/tts-*.jar 71 | ./tts-compose/build/outputs/aar/tts-compose-*.aar 72 | ./tts-compose/build/libs/tts-compose-*.klib 73 | ./tts-compose/build/libs/tts-compose-*.jar 74 | 75 | deploy-docs: 76 | runs-on: ubuntu-latest 77 | timeout-minutes: 8 78 | needs: build-and-deploy-lib 79 | steps: 80 | 81 | - name: Checkout repository 82 | uses: actions/checkout@v4 83 | 84 | - name: Set up JDK 17 85 | uses: actions/setup-java@v4 86 | with: 87 | java-version: 17 88 | distribution: 'temurin' 89 | cache: 'gradle' 90 | 91 | - name: Grant execute permission for gradlew 92 | run: chmod +x gradlew 93 | 94 | - name: Update version 95 | run: sed -i 's/tts[[:space:]]*=[[:space:]]*"[^"]*"/tts = "${{ github.event.inputs.version }}"/g' ./gradle/libs.versions.toml 96 | 97 | - name: Copy docs of previous versions 98 | uses: actions/checkout@v4 99 | with: 100 | ref: 'dokka-version-archive' 101 | path: './build/dokka/html_version_archive/' 102 | 103 | - name: Generate docs with Gradle & Dokka 104 | run: ./gradlew :dokkaGenerate 105 | 106 | - name: Upload docs 107 | uses: actions/upload-artifact@v4 108 | with: 109 | name: TextToSpeechKt.Docs 110 | path: ./out/* 111 | if-no-files-found: error 112 | 113 | - name: Deploy Dokka archive 114 | if: success() 115 | uses: crazy-max/ghaction-github-pages@v4 116 | with: 117 | target_branch: dokka-version-archive 118 | build_dir: ./build/dokka/html_version_archive/ 119 | env: 120 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 121 | 122 | - name: Deploy to GitHub Pages 123 | if: success() 124 | uses: crazy-max/ghaction-github-pages@v4 125 | with: 126 | target_branch: gh-pages 127 | build_dir: ./out/ 128 | env: 129 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 130 | 131 | release: 132 | runs-on: ubuntu-latest 133 | needs: [build-and-deploy-lib, deploy-docs] 134 | timeout-minutes: 8 135 | if: ${{ github.event.inputs.create_tag == 'true' }} 136 | steps: 137 | 138 | - name: Create tmp directory 139 | run: | 140 | mkdir out 141 | mkdir out/libs 142 | mkdir out/docs 143 | 144 | - name: Download library artifacts 145 | uses: actions/download-artifact@v4 146 | with: 147 | name: TextToSpeechKt.Libs 148 | path: ./out/libs/ 149 | 150 | - name: Download docs 151 | uses: actions/download-artifact@v4 152 | with: 153 | name: TextToSpeechKt.Docs 154 | path: ./out/docs/ 155 | 156 | - name: Zip archives 157 | run: | 158 | cd ./out/libs 159 | zip -r ../../TextToSpeechKt.Libs.zip ./ 160 | cd ../docs 161 | zip -r ../../TextToSpeechKt.Docs.zip ./ 162 | cd ../../ 163 | 164 | - name: Create tag 165 | uses: ncipollo/release-action@v1 166 | with: 167 | tag: v${{ github.event.inputs.version }} 168 | name: v${{ github.event.inputs.version }} 169 | body: | 170 | Release version ${{ github.event.inputs.version }} 171 | draft: true 172 | commit: ${{ github.sha }} 173 | artifacts: | 174 | TextToSpeechKt.Libs.zip 175 | TextToSpeechKt.Docs.zip 176 | -------------------------------------------------------------------------------- /tts/src/androidMain/kotlin/nl/marc_apps/tts/TextToSpeechAndroid.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package nl.marc_apps.tts 4 | 5 | import android.annotation.TargetApi 6 | import android.os.Build.VERSION 7 | import android.os.Build.VERSION_CODES 8 | import android.os.Bundle 9 | import androidx.annotation.ChecksSdkIntAtLeast 10 | import androidx.annotation.IntRange 11 | import androidx.annotation.RequiresApi 12 | import nl.marc_apps.tts.experimental.ExperimentalVoiceApi 13 | import nl.marc_apps.tts.utils.ResultHandler 14 | import nl.marc_apps.tts.utils.TtsProgressConverter 15 | import nl.marc_apps.tts.utils.VoiceAndroidLegacy 16 | import nl.marc_apps.tts.utils.VoiceAndroidModern 17 | import nl.marc_apps.tts.utils.getContinuationId 18 | import java.util.Locale 19 | import kotlin.uuid.ExperimentalUuidApi 20 | import kotlin.uuid.Uuid 21 | import android.speech.tts.TextToSpeech as AndroidTTS 22 | 23 | @OptIn(ExperimentalUuidApi::class) 24 | @TargetApi(VERSION_CODES.DONUT) 25 | internal class TextToSpeechAndroid(private var tts: AndroidTTS?) : TextToSpeech() { 26 | override val canDetectSynthesisStarted = hasModernProgressListeners 27 | 28 | private val internalVolume: Float 29 | get() = if(!isMuted) volume / 100f else 0f 30 | 31 | @IntRange(from = TextToSpeechInstance.VOLUME_MIN.toLong(), to = TextToSpeechInstance.VOLUME_MAX.toLong()) 32 | override var volume: Int = TextToSpeechInstance.VOLUME_DEFAULT 33 | set(value) { 34 | if(TextToSpeechFactory.canChangeVolume) { 35 | field = when { 36 | value < TextToSpeechInstance.VOLUME_MIN -> TextToSpeechInstance.VOLUME_MIN 37 | value > TextToSpeechInstance.VOLUME_MAX -> TextToSpeechInstance.VOLUME_MAX 38 | else -> value 39 | } 40 | } 41 | } 42 | 43 | override var isMuted: Boolean = false 44 | 45 | override var pitch: Float = TextToSpeechInstance.VOICE_PITCH_DEFAULT 46 | set(value) { 47 | field = value 48 | tts?.setPitch(value) 49 | } 50 | 51 | override var rate: Float = TextToSpeechInstance.VOICE_RATE_DEFAULT 52 | set(value) { 53 | field = value 54 | tts?.setSpeechRate(value) 55 | } 56 | 57 | private val voiceLocale: Locale 58 | get() = (if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) tts?.voice?.locale else tts?.language) ?: Locale.getDefault() 59 | 60 | override val language: String 61 | get() = if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) voiceLocale.toLanguageTag() else voiceLocale.language 62 | 63 | @ExperimentalVoiceApi 64 | private val defaultVoice: Voice? = if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP){ 65 | (tts?.voice ?: tts?.defaultVoice)?.let { VoiceAndroidModern(it, true) } 66 | } else { 67 | tts?.language?.let { VoiceAndroidLegacy(it, true) } 68 | } 69 | 70 | @ExperimentalVoiceApi 71 | override var currentVoice: Voice? = defaultVoice 72 | set(value) { 73 | val result = if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && value is VoiceAndroidModern) { 74 | tts?.setVoice(value.androidVoice) 75 | } else if (value is VoiceAndroidLegacy) { 76 | tts?.setLanguage(value.locale) 77 | } else null 78 | if (result != null && result >= 0) { 79 | field = value 80 | } 81 | } 82 | 83 | @ExperimentalVoiceApi 84 | override val voices: Sequence = if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 85 | (tts?.voices ?: emptySet()).asSequence().map { 86 | VoiceAndroidModern(it, it == (defaultVoice as? VoiceAndroidModern)?.androidVoice) 87 | } 88 | } else { 89 | Locale.getAvailableLocales().asSequence().filter { 90 | val availability = tts?.isLanguageAvailable(it) 91 | availability != AndroidTTS.LANG_NOT_SUPPORTED && availability != AndroidTTS.LANG_MISSING_DATA 92 | }.map { 93 | VoiceAndroidLegacy(it, it == defaultVoice?.locale) 94 | } 95 | } 96 | 97 | init { 98 | if (hasModernProgressListeners) { 99 | setProgressListeners() 100 | } else { 101 | setProgressListenersLegacy() 102 | } 103 | } 104 | 105 | private fun setProgressListenersLegacy() { 106 | tts?.setOnUtteranceCompletedListener { 107 | val id = getContinuationId(it) ?: return@setOnUtteranceCompletedListener 108 | onTtsCompleted(id, Result.success(Unit)) 109 | } 110 | } 111 | 112 | @RequiresApi(VERSION_CODES.ICE_CREAM_SANDWICH_MR1) 113 | private fun setProgressListeners() { 114 | tts?.setOnUtteranceProgressListener(TtsProgressConverter(::onTtsStarted, ::onTtsCompleted)) 115 | } 116 | 117 | override fun enqueueInternal(text: String, resultHandler: ResultHandler) { 118 | val utteranceId = Uuid.random() 119 | val utteranceIdString = utteranceId.toString() 120 | 121 | callbackHandler.add(utteranceId, utteranceIdString, resultHandler) 122 | 123 | if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 124 | val params = Bundle() 125 | params.putFloat(KEY_PARAM_VOLUME, internalVolume) 126 | params.putString(KEY_PARAM_UTTERANCE_ID, utteranceIdString) 127 | tts?.speak(text, AndroidTTS.QUEUE_ADD, params, utteranceIdString) 128 | } else { 129 | tts?.speak(text, AndroidTTS.QUEUE_ADD, hashMapOf( 130 | KEY_PARAM_VOLUME to internalVolume.toString(), 131 | KEY_PARAM_UTTERANCE_ID to utteranceIdString 132 | )) 133 | } 134 | } 135 | 136 | override fun stop() { 137 | tts?.stop() 138 | super.stop() 139 | } 140 | 141 | override fun close() { 142 | super.close() 143 | if (hasModernProgressListeners) { 144 | tts?.setOnUtteranceProgressListener(null) 145 | } else { 146 | tts?.setOnUtteranceCompletedListener(null) 147 | } 148 | tts?.shutdown() 149 | tts = null 150 | } 151 | 152 | companion object { 153 | private const val KEY_PARAM_VOLUME = "volume" 154 | 155 | private const val KEY_PARAM_UTTERANCE_ID = "utteranceId" 156 | 157 | @ChecksSdkIntAtLeast(api = VERSION_CODES.ICE_CREAM_SANDWICH_MR1) 158 | private val hasModernProgressListeners = VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH_MR1 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/androidMain/resources/layout/fragment_main_xml.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 19 | 20 | 21 | 22 | 28 | 29 | 32 | 33 | 51 | 52 | 55 | 56 | 57 | 58 | 71 | 72 | 89 | 90 | 103 | 104 | 116 | 117 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 4 | import org.jetbrains.compose.resources.ResourcesExtension.ResourceClassGeneration 5 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 6 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 7 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 8 | import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig 9 | 10 | plugins { 11 | alias(libs.plugins.kotlin.multiplatform) 12 | alias(libs.plugins.android.application) 13 | alias(libs.plugins.compose) 14 | alias(libs.plugins.compose.compiler) 15 | alias(libs.plugins.ksp) 16 | } 17 | 18 | kotlin { 19 | androidTarget { 20 | compilerOptions { 21 | jvmTarget = JvmTarget.JVM_1_8 22 | } 23 | 24 | dependencies { 25 | debugImplementation(compose.uiTooling) 26 | } 27 | } 28 | 29 | listOf( 30 | iosX64(), 31 | iosArm64(), 32 | iosSimulatorArm64(), 33 | macosArm64(), 34 | macosX64() 35 | ).forEach { appleTarget -> 36 | appleTarget.binaries.framework { 37 | baseName = "ComposeApp" 38 | } 39 | } 40 | 41 | js { 42 | moduleName = "compose-multiplatform" 43 | browser { 44 | commonWebpackConfig { 45 | devServer = devServer ?: KotlinWebpackConfig.DevServer() 46 | } 47 | } 48 | binaries.executable() 49 | useEsModules() 50 | } 51 | 52 | @OptIn(ExperimentalWasmDsl::class) 53 | wasmJs { 54 | moduleName = "compose-multiplatform" 55 | browser { 56 | commonWebpackConfig { 57 | devServer = devServer ?: KotlinWebpackConfig.DevServer() 58 | } 59 | } 60 | binaries.executable() 61 | } 62 | 63 | jvm { 64 | compilerOptions { 65 | jvmTarget = JvmTarget.JVM_17 66 | } 67 | } 68 | 69 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 70 | applyDefaultHierarchyTemplate { 71 | common { 72 | group("webCommonW3C") { 73 | withJs() 74 | withWasmJs() 75 | } 76 | } 77 | } 78 | 79 | compilerOptions { 80 | freeCompilerArgs.add("-Xexpect-actual-classes") 81 | } 82 | 83 | sourceSets { 84 | all { 85 | languageSettings { 86 | optIn("nl.marc_apps.tts.experimental.ExperimentalVoiceApi") 87 | optIn("nl.marc_apps.tts.experimental.ExperimentalDesktopTarget") 88 | } 89 | } 90 | 91 | commonMain.dependencies { 92 | implementation(compose.runtime) 93 | implementation(compose.ui) 94 | implementation(compose.foundation) 95 | implementation(compose.material3) 96 | implementation(compose.materialIconsExtended) 97 | // implementation(compose.components.uiToolingPreview) 98 | implementation(compose.components.resources) 99 | implementation(libs.kotlin.coroutines) 100 | implementation(projects.ttsCompose) 101 | } 102 | 103 | androidMain.dependencies { 104 | implementation(compose.preview) 105 | implementation(compose.uiTooling) 106 | 107 | implementation("androidx.activity:activity-compose:1.9.0") 108 | 109 | implementation(libs.androidx.appcompat) 110 | implementation(libs.androidx.constraintlayout) 111 | implementation(libs.androidx.fragment) 112 | implementation(libs.androidx.lifecycle.runtime) 113 | 114 | implementation(libs.google.material.xml) 115 | 116 | implementation(libs.koin.android) 117 | implementation(libs.koin.androidx.startup) 118 | implementation(libs.koin.annotations) 119 | } 120 | 121 | jvmMain.dependencies { 122 | implementation(compose.preview) 123 | implementation(compose.uiTooling) 124 | implementation(compose.desktop.currentOs) 125 | } 126 | 127 | wasmJsMain.dependencies { 128 | implementation(libs.kotlin.browser) 129 | } 130 | } 131 | } 132 | 133 | dependencies { 134 | implementation(platform(libs.koin.bom)) 135 | implementation(platform(libs.koin.annotations.bom)) 136 | add("kspAndroid", libs.koin.annotations.ksp) 137 | } 138 | 139 | android { 140 | compileSdk = libs.versions.android.compileSdk.get().toInt() 141 | buildToolsVersion = libs.versions.android.buildTools.get() 142 | 143 | namespace = "nl.marc_apps.tts_demo" 144 | 145 | sourceSets { 146 | named("main") { 147 | manifest.srcFile("src/androidMain/AndroidManifest.xml") 148 | res.srcDirs("src/androidMain/resources", "src/commonMain/resources") 149 | } 150 | } 151 | 152 | packaging { 153 | jniLibs { 154 | excludes += setOf("kotlin/**") 155 | } 156 | 157 | resources { 158 | excludes += setOf( 159 | "kotlin/**", 160 | "**/*.kotlin_metadata", 161 | "META-INF/*.kotlin_module", 162 | // "META-INF/*.version", 163 | "META-INF/AL2.0", 164 | "META-INF/LGPL2.1", 165 | "DebugProbesKt.bin", 166 | "build-data.properties", 167 | "play-**.properties", 168 | "kotlin-tooling-metadata.json" 169 | ) 170 | } 171 | } 172 | 173 | defaultConfig { 174 | applicationId = "nl.marc_apps.tts_demo" 175 | 176 | minSdk = 21 177 | targetSdk = libs.versions.android.compileSdk.get().toInt() 178 | 179 | versionCode = 1 180 | versionName = libs.versions.tts.get() 181 | 182 | testBuildType = "debug" 183 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 184 | } 185 | 186 | buildTypes { 187 | named("release") { 188 | isMinifyEnabled = true 189 | isShrinkResources = true 190 | 191 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 192 | } 193 | 194 | named("debug") { 195 | applicationIdSuffix = ".debug" 196 | 197 | isMinifyEnabled = false 198 | isShrinkResources = false 199 | 200 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 201 | } 202 | } 203 | 204 | bundle { 205 | abi.enableSplit = true 206 | language.enableSplit = true 207 | density.enableSplit = true 208 | texture.enableSplit = true 209 | deviceTier.enableSplit = true 210 | } 211 | 212 | buildFeatures { 213 | viewBinding = true 214 | } 215 | 216 | testOptions { 217 | unitTests { 218 | isIncludeAndroidResources = true 219 | } 220 | } 221 | } 222 | 223 | compose.resources { 224 | publicResClass = false 225 | packageOfResClass = "nl.marc_apps.tts_demo.resources" 226 | generateResClass = ResourceClassGeneration.Always 227 | } 228 | 229 | compose.desktop { 230 | application { 231 | buildTypes { 232 | release { 233 | proguard { 234 | isEnabled.set(true) 235 | obfuscate.set(true) 236 | } 237 | } 238 | } 239 | mainClass = "MainKt" 240 | nativeDistributions { 241 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 242 | packageName = "TTS Demo" 243 | packageVersion = libs.versions.tts.get().substringBefore("-") 244 | windows { 245 | perUserInstall = true 246 | } 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /demo/compose-multiplatform/src/commonMain/kotlin/nl/marc_apps/tts_demo/OptionsCard.kt: -------------------------------------------------------------------------------- 1 | package nl.marc_apps.tts_demo 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.foundation.lazy.items 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.automirrored.rounded.VolumeMute 9 | import androidx.compose.material.icons.automirrored.rounded.VolumeUp 10 | import androidx.compose.material.icons.rounded.* 11 | import androidx.compose.material3.* 12 | import androidx.compose.runtime.* 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.unit.dp 16 | import nl.marc_apps.tts.TextToSpeechInstance 17 | import org.jetbrains.compose.resources.stringResource 18 | import kotlin.math.roundToInt 19 | import nl.marc_apps.tts_demo.resources.* 20 | 21 | @OptIn(ExperimentalMaterial3Api::class) 22 | @Composable 23 | fun OptionsCard( 24 | textToSpeech: TextToSpeechInstance 25 | ) { 26 | var expanded by remember { mutableStateOf(false) } 27 | 28 | Surface( 29 | color = MaterialTheme.colorScheme.surface, 30 | shape = MaterialTheme.shapes.medium, 31 | tonalElevation = if (expanded) 6.dp else 2.dp 32 | ) { 33 | Column( 34 | verticalArrangement = Arrangement.spacedBy(8.dp) 35 | ) { 36 | Row( 37 | horizontalArrangement = Arrangement.spacedBy(16.dp), 38 | verticalAlignment = Alignment.CenterVertically, 39 | modifier = Modifier 40 | .clickable { 41 | expanded = !expanded 42 | } 43 | .padding(16.dp, 8.dp) 44 | .height(48.dp) 45 | ) { 46 | Icon(Icons.Rounded.Settings, contentDescription = null) 47 | 48 | Text( 49 | stringResource(Res.string.options_title), 50 | style = MaterialTheme.typography.bodyMedium, 51 | modifier = Modifier.weight(1f) 52 | ) 53 | 54 | Icon( 55 | if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore, 56 | contentDescription = null 57 | ) 58 | } 59 | 60 | if (expanded){ 61 | HorizontalDivider() 62 | 63 | Row ( 64 | horizontalArrangement = Arrangement.spacedBy(16.dp), 65 | verticalAlignment = Alignment.CenterVertically, 66 | modifier = Modifier 67 | .padding(16.dp, 8.dp) 68 | .height(48.dp) 69 | ) { 70 | Icon(Icons.AutoMirrored.Rounded.VolumeMute, contentDescription = null) 71 | 72 | var preferredSoundLevel by remember { mutableStateOf(textToSpeech.volume.toFloat()) } 73 | 74 | Slider( 75 | value = preferredSoundLevel, 76 | onValueChange = { 77 | preferredSoundLevel = it.roundToInt().toFloat() 78 | textToSpeech.volume = it.roundToInt() 79 | }, 80 | valueRange = 0f..100f, 81 | steps = 10, 82 | modifier = Modifier.weight(1f) 83 | ) 84 | 85 | Icon(Icons.AutoMirrored.Rounded.VolumeUp, contentDescription = null) 86 | } 87 | 88 | Row ( 89 | horizontalArrangement = Arrangement.spacedBy(16.dp), 90 | verticalAlignment = Alignment.CenterVertically, 91 | modifier = Modifier 92 | .padding(16.dp, 8.dp) 93 | .height(48.dp) 94 | ) { 95 | Icon(Icons.Rounded.Speed, contentDescription = null) 96 | 97 | var preferredRate by remember { mutableStateOf(textToSpeech.rate * 10) } 98 | 99 | Slider( 100 | value = preferredRate, 101 | onValueChange = { 102 | preferredRate = it 103 | textToSpeech.rate = it * 0.1f 104 | }, 105 | valueRange = 1f..20f, 106 | steps = 20, 107 | modifier = Modifier.weight(1f) 108 | ) 109 | } 110 | 111 | Row ( 112 | horizontalArrangement = Arrangement.spacedBy(16.dp), 113 | verticalAlignment = Alignment.CenterVertically, 114 | modifier = Modifier 115 | .padding(16.dp, 8.dp) 116 | .height(48.dp) 117 | ) { 118 | Icon(Icons.Rounded.Moving, contentDescription = null) 119 | 120 | var preferredPitch by remember { mutableStateOf(textToSpeech.pitch * 10 - 5) } 121 | 122 | Slider( 123 | value = preferredPitch, 124 | onValueChange = { 125 | preferredPitch = it 126 | textToSpeech.pitch = it * 0.1f + 0.5f 127 | }, 128 | valueRange = 1f..10f, 129 | steps = 10, 130 | modifier = Modifier.weight(1f) 131 | ) 132 | } 133 | 134 | var showDialog by remember { mutableStateOf(false) } 135 | 136 | Row ( 137 | horizontalArrangement = Arrangement.spacedBy(16.dp), 138 | verticalAlignment = Alignment.CenterVertically, 139 | modifier = Modifier 140 | .padding(16.dp, 8.dp) 141 | .height(48.dp) 142 | .fillMaxWidth() 143 | .clickable { 144 | showDialog = true 145 | } 146 | ) { 147 | Icon(Icons.Rounded.Language, contentDescription = null) 148 | 149 | Text( 150 | textToSpeech.currentVoice?.let { "${it.languageTag} (${it.name})" } ?: stringResource(Res.string.placeholder_voice_unknown), 151 | style = MaterialTheme.typography.bodySmall 152 | ) 153 | } 154 | 155 | Row ( 156 | horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.End), 157 | verticalAlignment = Alignment.CenterVertically, 158 | modifier = Modifier 159 | .padding(16.dp, 8.dp) 160 | .height(48.dp) 161 | .fillMaxWidth() 162 | ) { 163 | Button( 164 | onClick = { 165 | textToSpeech.volume = TextToSpeechInstance.VOLUME_DEFAULT 166 | textToSpeech.pitch = TextToSpeechInstance.VOICE_PITCH_DEFAULT 167 | textToSpeech.rate = TextToSpeechInstance.VOICE_RATE_DEFAULT 168 | textToSpeech.currentVoice = textToSpeech.voices.firstOrNull { it.isDefault } 169 | expanded = false 170 | } 171 | ) { 172 | Text(stringResource(Res.string.action_options_reset_all)) 173 | } 174 | } 175 | 176 | if (showDialog) { 177 | BasicAlertDialog( 178 | onDismissRequest = { 179 | showDialog = false 180 | }, 181 | modifier = Modifier.defaultMinSize(160.dp, 160.dp) 182 | ) { 183 | Surface( 184 | tonalElevation = 8.dp 185 | ) { 186 | LazyColumn { 187 | items(textToSpeech.voices.toList(), key = { it.hashCode() }) { 188 | Text( 189 | "${it.languageTag} (${it.name})", 190 | maxLines = 1, 191 | modifier = Modifier 192 | .padding(16.dp, 4.dp) 193 | .clickable { 194 | textToSpeech.currentVoice = it 195 | showDialog = false 196 | } 197 | ) 198 | } 199 | } 200 | } 201 | } 202 | } 203 | } 204 | } 205 | } 206 | } 207 | --------------------------------------------------------------------------------