From 746aff6a05be7d9c542f0aa41a21218400811654 Mon Sep 17 00:00:00 2001 From: fenghezhou Date: Tue, 11 Nov 2025 11:07:45 +0800 Subject: [PATCH 1/5] refactor: use pigeon for dart <-> platform communication --- .vscode/launch.json | 6 +- .vscode/settings.json | 7 + android/build.gradle | 67 +- android/gradle.properties | 3 - .../gradle/wrapper/gradle-wrapper.properties | 3 +- android/src/main/AndroidManifest.xml | 13 +- .../tundralabs/fluttertts/FlutterTtsPlugin.kt | 1007 +++++---- .../com/tundralabs/fluttertts/messages.g.kt | 1670 ++++++++++++++ build.yaml | 2 + example/.metadata | 26 +- example/analysis_options.yaml | 28 + example/android/app/build.gradle | 49 - example/android/app/build.gradle.kts | 44 + example/android/build.gradle | 18 - example/android/build.gradle.kts | 24 + example/android/gradle.properties | 7 +- .../gradle/wrapper/gradle-wrapper.properties | 3 +- example/android/settings.gradle | 25 - example/android/settings.gradle.kts | 28 + example/android/settings_aar.gradle | 1 - example/ios/.gitignore | 64 +- example/ios/Flutter/.last_build_id | 1 - example/ios/Flutter/AppFrameworkInfo.plist | 6 +- example/ios/Flutter/Debug.xcconfig | 2 +- example/ios/Flutter/Release.xcconfig | 2 +- example/ios/Podfile | 6 +- example/ios/Runner.xcodeproj/project.pbxproj | 349 ++- .../xcshareddata/xcschemes/Runner.xcscheme | 26 +- example/ios/Runner/AppDelegate.swift | 6 +- .../Icon-App-1024x1024@1x.png | Bin 11112 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 1418 bytes example/ios/Runner/Info.plist | 14 +- example/ios/Runner/Runner-Bridging-Header.h | 2 +- example/ios/RunnerTests/RunnerTests.swift | 12 + example/lib/main.dart | 471 ++-- example/macos/.gitignore | 2 +- example/macos/Flutter/Flutter-Debug.xcconfig | 2 +- .../macos/Flutter/Flutter-Release.xcconfig | 2 +- .../Flutter/GeneratedPluginRegistrant.swift | 12 + example/macos/Podfile | 4 +- .../macos/Runner.xcodeproj/project.pbxproj | 254 ++- .../xcshareddata/xcschemes/Runner.xcscheme | 22 +- example/macos/Runner/AppDelegate.swift | 6 +- .../AppIcon.appiconset/app_icon_1024.png | Bin 46993 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 3276 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 1429 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 5933 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1243 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 14800 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 1874 -> 2218 bytes example/macos/Runner/Base.lproj/MainMenu.xib | 4 + example/macos/Runner/Configs/AppInfo.xcconfig | 6 +- example/macos/Runner/MainFlutterWindow.swift | 2 +- example/macos/RunnerTests/RunnerTests.swift | 12 + example/pubspec.yaml | 20 +- ios/Classes/AudioCategory.swift | 29 +- ios/Classes/AudioCategoryOptions.swift | 64 +- ios/Classes/AudioModes.swift | 106 +- ios/Classes/FlutterTtsPlugin.h | 4 - ios/Classes/FlutterTtsPlugin.m | 15 - ios/Classes/SwiftFlutterTtsPlugin.swift | 961 +++++---- ios/Classes/message.g.swift | 1548 +++++++++++++ ios/flutter_tts.podspec | 1 - lib/flutter_tts.dart | 671 +----- lib/src/flutter_tts.dart | 5 + lib/src/flutter_tts_android.dart | 180 ++ lib/src/flutter_tts_ios.dart | 107 + lib/src/flutter_tts_macos.dart | 53 + lib/src/flutter_tts_method_channel.dart | 167 ++ lib/src/flutter_tts_native.dart | 4 + lib/src/flutter_tts_platform_interface.dart | 81 + lib/{ => src}/flutter_tts_web.dart | 233 +- .../flutter_tts_web_interop_types.dart} | 0 lib/src/messages.g.dart | 1917 +++++++++++++++++ macos/Classes/FlutterTtsPlugin.swift | 797 +++---- macos/Classes/message.g.swift | 1548 +++++++++++++ pigeons/messages.dart | 513 +++++ pubspec.yaml | 29 +- tools/generate_pigeons.dart | 60 + windows/CMakeLists.txt | 6 +- windows/flutter_tts_plugin.cpp | 1198 +++++----- windows/messages.g.cpp | 1848 ++++++++++++++++ windows/messages.g.h | 740 +++++++ 96 files changed, 13998 insertions(+), 3227 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 android/gradle.properties create mode 100644 android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt create mode 100644 build.yaml create mode 100644 example/analysis_options.yaml delete mode 100644 example/android/app/build.gradle create mode 100644 example/android/app/build.gradle.kts delete mode 100644 example/android/build.gradle create mode 100644 example/android/build.gradle.kts delete mode 100644 example/android/settings.gradle create mode 100644 example/android/settings.gradle.kts delete mode 100644 example/android/settings_aar.gradle delete mode 100644 example/ios/Flutter/.last_build_id create mode 100644 example/ios/RunnerTests/RunnerTests.swift create mode 100644 example/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 example/macos/RunnerTests/RunnerTests.swift delete mode 100644 ios/Classes/FlutterTtsPlugin.h delete mode 100644 ios/Classes/FlutterTtsPlugin.m create mode 100644 ios/Classes/message.g.swift create mode 100644 lib/src/flutter_tts.dart create mode 100644 lib/src/flutter_tts_android.dart create mode 100644 lib/src/flutter_tts_ios.dart create mode 100644 lib/src/flutter_tts_macos.dart create mode 100644 lib/src/flutter_tts_method_channel.dart create mode 100644 lib/src/flutter_tts_native.dart create mode 100644 lib/src/flutter_tts_platform_interface.dart rename lib/{ => src}/flutter_tts_web.dart (52%) rename lib/{interop_types.dart => src/flutter_tts_web_interop_types.dart} (100%) create mode 100644 lib/src/messages.g.dart create mode 100644 macos/Classes/message.g.swift create mode 100644 pigeons/messages.dart create mode 100644 tools/generate_pigeons.dart create mode 100644 windows/messages.g.cpp create mode 100644 windows/messages.g.h diff --git a/.vscode/launch.json b/.vscode/launch.json index 024463d7..72fc8aa3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "type": "dart", "request": "launch", "cwd": "example", - "program": "lib\\main.dart", + "program": "lib/main.dart", "flutterMode": "debug" }, { @@ -13,7 +13,7 @@ "type": "dart", "request": "launch", "cwd": "example", - "program": "lib\\main.dart", + "program": "lib/main.dart", "flutterMode": "release" }, { @@ -21,7 +21,7 @@ "type": "dart", "request": "launch", "cwd": "example", - "program": "lib\\main.dart", + "program": "lib/main.dart", "flutterMode": "profile" } ] diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..cf604961 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "files.associations": { + "*.arb": "json", + "xstring": "cpp", + "string": "cpp" + } +} diff --git a/android/build.gradle b/android/build.gradle index 24d029a9..1850753a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,64 +1,61 @@ -def args = ["-Xlint:deprecation"] - -group 'com.tundralabs.fluttertts' -version '1.0-SNAPSHOT' +group = "com.tundralabs.fluttertts" +version = "1.0-SNAPSHOT" buildscript { - ext.kotlin_version = '1.9.10' + ext.kotlin_version = "2.1.0" repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.2.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath("com.android.tools.build:gradle:8.9.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") } } -rootProject.allprojects { +allprojects { repositories { google() mavenCentral() } } -project.getTasks().withType(JavaCompile).configureEach { - options.compilerArgs.addAll(args) -} - -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +apply plugin: "com.android.library" +apply plugin: "kotlin-android" android { - compileSdk 34 - if (project.android.hasProperty("namespace")) { - namespace 'com.tundralabs.fluttertts' + namespace = "m.tundralabs.fluttertts" + + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } - defaultConfig { - minSdkVersion 21 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' + defaultConfig { + minSdk = 24 } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") } - kotlinOptions { - jvmTarget = '1.8' + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } -dependencies { - implementation "androidx.core:core-ktx:1.8.0" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" -} -repositories { - mavenCentral() -} diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index 94adc3a3..00000000 --- a/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true -android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 4310b9d6..f14a403e 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip +#distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.13-all.zip diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index a88a091e..21c699c7 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,10 @@ - + + + + + + + + + + \ No newline at end of file diff --git a/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt b/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt index 36fd1421..68f5a4e2 100644 --- a/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt +++ b/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt @@ -5,7 +5,6 @@ import android.content.Context import android.media.AudioAttributes import android.media.AudioFocusRequest import android.media.AudioManager -import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Environment @@ -13,30 +12,30 @@ import android.os.Handler import android.os.Looper import android.os.ParcelFileDescriptor import android.provider.MediaStore -import android.provider.OpenableColumns import android.speech.tts.TextToSpeech import android.speech.tts.UtteranceProgressListener import android.speech.tts.Voice import io.flutter.Log import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result import java.io.File import java.lang.reflect.Field import java.util.Locale import java.util.MissingResourceException import java.util.UUID +typealias ResultCallback = (Result) -> Unit + +fun FlutterTtsErrorCode.toKtResult(): Result { + return Result.failure(FlutterError("FlutterTtsErrorCode.$raw", name)) +} /** FlutterTtsPlugin */ -class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { +class FlutterTtsPlugin : FlutterPlugin, TtsHostApi, AndroidTtsHostApi { + private val kTtsInitTimeOutMs: Long = 1000 + private var handler: Handler? = null - private var methodChannel: MethodChannel? = null - private var speakResult: Result? = null - private var synthResult: Result? = null + private var speakResult: ResultCallback? = null + private var synthResult: ResultCallback? = null private var awaitSpeakCompletion = false private var speaking = false private var awaitSynthCompletion = false @@ -47,7 +46,7 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { private val pendingMethodCalls = ArrayList() private val utterances = HashMap() private var bundle: Bundle? = null - private var silencems = 0 + private var silenceMs = 0 private var lastProgress = 0 private var currentText: String? = null private var pauseText: String? = null @@ -55,50 +54,55 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { private var queueMode: Int = TextToSpeech.QUEUE_FLUSH private var ttsStatus: Int? = null private var selectedEngine: String? = null - private var engineResult: Result? = null + private var engineResult: ResultCallback? = null private var parcelFileDescriptor: ParcelFileDescriptor? = null private var audioManager: AudioManager? = null private var audioFocusRequest: AudioFocusRequest? = null + private var flutterApi: TtsFlutterApi? = null + companion object { private const val SILENCE_PREFIX = "SIL_" private const val SYNTHESIZE_TO_FILE_PREFIX = "STF_" } - private fun initInstance(messenger: BinaryMessenger, context: Context) { + private fun initInstance(context: Context) { this.context = context - methodChannel = MethodChannel(messenger, "flutter_tts") - methodChannel!!.setMethodCallHandler(this) handler = Handler(Looper.getMainLooper()) bundle = Bundle() + + handler?.postDelayed(onInitTimeoutRunnable, kTtsInitTimeOutMs) tts = TextToSpeech(context, onInitListenerWithoutCallback) } /** Android Plugin APIs */ override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - initInstance(binding.binaryMessenger, binding.applicationContext) + flutterApi = TtsFlutterApi(binding.binaryMessenger) + initInstance(binding.applicationContext) + TtsHostApi.setUp(binding.binaryMessenger, this) + AndroidTtsHostApi.setUp(binding.binaryMessenger, this) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - stop() + stopImpl() tts!!.shutdown() + TtsHostApi.setUp(binding.binaryMessenger, null) + AndroidTtsHostApi.setUp(binding.binaryMessenger, null) context = null - methodChannel!!.setMethodCallHandler(null) - methodChannel = null } private val utteranceProgressListener: UtteranceProgressListener = object : UtteranceProgressListener() { override fun onStart(utteranceId: String) { if (utteranceId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) { - invokeMethod("synth.onStart", true) + flutterApi?.onSynthStartCb { } } else { if (isPaused) { - invokeMethod("speak.onContinue", true) + flutterApi?.onSpeakResumeCb { } isPaused = false } else { Log.d(tag, "Utterance ID has started: $utteranceId") - invokeMethod("speak.onStart", true) + flutterApi?.onSpeakStartCb { } } } if (Build.VERSION.SDK_INT < 26) { @@ -114,13 +118,13 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { if (awaitSynthCompletion) { synthCompletion(1) } - invokeMethod("synth.onComplete", true) + flutterApi?.onSynthCompleteCb { } } else { Log.d(tag, "Utterance ID has completed: $utteranceId") if (awaitSpeakCompletion && queueMode == TextToSpeech.QUEUE_FLUSH) { speakCompletion(1) } - invokeMethod("speak.onComplete", true) + flutterApi?.onSpeakCompleteCb { } } lastProgress = 0 pauseText = null @@ -130,16 +134,15 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { override fun onStop(utteranceId: String, interrupted: Boolean) { Log.d( - tag, - "Utterance ID has been stopped: $utteranceId. Interrupted: $interrupted" + tag, "Utterance ID has been stopped: $utteranceId. Interrupted: $interrupted" ) if (awaitSpeakCompletion) { speaking = false } if (isPaused) { - invokeMethod("speak.onPause", true) + flutterApi?.onSpeakPauseCb { } } else { - invokeMethod("speak.onCancel", true) + flutterApi?.onSpeakCancelCb { } } releaseAudioFocus() } @@ -147,12 +150,15 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { private fun onProgress(utteranceId: String?, startAt: Int, endAt: Int) { if (utteranceId != null && !utteranceId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) { val text = utterances[utteranceId] - val data = HashMap() - data["text"] = text - data["start"] = startAt.toString() - data["end"] = endAt.toString() - data["word"] = text!!.substring(startAt, endAt) - invokeMethod("speak.onProgress", data) + if (text != null) { + var data = TtsProgress( + text = text, + start = startAt.toLong(), + end = endAt.toLong(), + word = text.substring(startAt, endAt) + ) + flutterApi?.onSpeakProgressCb(data) { } + } } } @@ -165,19 +171,19 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { } } - @Deprecated("") + @Deprecated("Deprecated in Java") override fun onError(utteranceId: String) { if (utteranceId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) { closeParcelFileDescriptor(true) if (awaitSynthCompletion) { synth = false } - invokeMethod("synth.onError", "Error from TextToSpeech (synth)") + flutterApi?.onSynthErrorCb("Error from TextToSpeech (synth)") {} } else { if (awaitSpeakCompletion) { speaking = false } - invokeMethod("speak.onError", "Error from TextToSpeech (speak)") + flutterApi?.onSpeakErrorCb("Error from TextToSpeech (speak)") {} } releaseAudioFocus() } @@ -188,12 +194,12 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { if (awaitSynthCompletion) { synth = false } - invokeMethod("synth.onError", "Error from TextToSpeech (synth) - $errorCode") + flutterApi?.onSynthErrorCb("Error from TextToSpeech (synth) - $errorCode") {} } else { if (awaitSpeakCompletion) { speaking = false } - invokeMethod("speak.onError", "Error from TextToSpeech (speak) - $errorCode") + flutterApi?.onSpeakErrorCb("Error from TextToSpeech (speak) - $errorCode") {} } } } @@ -201,7 +207,7 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { fun speakCompletion(success: Int) { speaking = false handler!!.post { - speakResult?.success(success) + speakResult?.invoke(Result.success(TtsResult(success != 0))) speakResult = null } } @@ -209,13 +215,14 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { fun synthCompletion(success: Int) { synth = false handler!!.post { - synthResult?.success(success) + synthResult?.invoke(Result.success(TtsResult(success != 0))) synthResult = null } } private val onInitListenerWithCallback: TextToSpeech.OnInitListener = TextToSpeech.OnInitListener { status -> + handler?.removeCallbacks(onInitTimeoutRunnable) // Handle pending method calls (sent while TTS was initializing) synchronized(this@FlutterTtsPlugin) { ttsStatus = status @@ -229,7 +236,7 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { tts!!.setOnUtteranceProgressListener(utteranceProgressListener) try { val locale: Locale = tts!!.defaultVoice.locale - if (isLanguageAvailable(locale)) { + if (isLanguageAvailableImpl(locale)) { tts!!.language = locale } } catch (e: NullPointerException) { @@ -238,15 +245,30 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { Log.e(tag, "getDefaultLocale: " + e.message) } - engineResult!!.success(1) + engineResult?.invoke(Result.success(TtsResult(true))) } else { - engineResult!!.error("TtsError","Failed to initialize TextToSpeech with status: $status", null) + engineResult?.invoke( + FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult() + ) } //engineResult = null } + private val onInitTimeoutRunnable = Runnable { + Log.e("TTS", "TTS init timeout") + + engineResult?.invoke(FlutterTtsErrorCode.TTS_INIT_TIMEOUT.toKtResult()) + + ttsStatus = TextToSpeech.ERROR + for (call in pendingMethodCalls) { + call.run() + } + pendingMethodCalls.clear() + } + private val onInitListenerWithoutCallback: TextToSpeech.OnInitListener = TextToSpeech.OnInitListener { status -> + handler?.removeCallbacks(onInitTimeoutRunnable) // Handle pending method calls (sent while TTS was initializing) synchronized(this@FlutterTtsPlugin) { ttsStatus = status @@ -260,7 +282,7 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { tts!!.setOnUtteranceProgressListener(utteranceProgressListener) try { val locale: Locale = tts!!.defaultVoice.locale - if (isLanguageAvailable(locale)) { + if (isLanguageAvailableImpl(locale)) { tts!!.language = locale } } catch (e: NullPointerException) { @@ -273,212 +295,25 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { } } - override fun onMethodCall(call: MethodCall, result: Result) { - // If TTS is still loading - synchronized(this@FlutterTtsPlugin) { - if (ttsStatus == null) { - // Suspend method call until the TTS engine is ready - val suspendedCall = Runnable { onMethodCall(call, result) } - pendingMethodCalls.add(suspendedCall) - return - } - } - when (call.method) { - "speak" -> { - var text: String = call.argument("text")!! - val focus: Boolean = call.argument("focus")!! - if (pauseText == null) { - pauseText = text - currentText = pauseText!! - } - if (isPaused) { - // Ensure the text hasn't changed - if (currentText == text) { - text = pauseText!! - } else { - pauseText = text - currentText = pauseText!! - lastProgress = 0 - } - } - if (speaking) { - // If TTS is set to queue mode, allow the utterance to be queued up rather than discarded - if (queueMode == TextToSpeech.QUEUE_FLUSH) { - result.success(0) - return - } - } - val b = speak(text, focus) - if (!b) { - synchronized(this@FlutterTtsPlugin) { - val suspendedCall = Runnable { onMethodCall(call, result) } - pendingMethodCalls.add(suspendedCall) - } - return - } - // Only use await speak completion if queueMode is set to QUEUE_FLUSH - if (awaitSpeakCompletion && queueMode == TextToSpeech.QUEUE_FLUSH) { - speaking = true - speakResult = result - } else { - result.success(1) - } - } - - "awaitSpeakCompletion" -> { - awaitSpeakCompletion = java.lang.Boolean.parseBoolean(call.arguments.toString()) - result.success(1) - } - - "awaitSynthCompletion" -> { - awaitSynthCompletion = java.lang.Boolean.parseBoolean(call.arguments.toString()) - result.success(1) - } - - "getMaxSpeechInputLength" -> { - val res = maxSpeechInputLength - result.success(res) - } - - "synthesizeToFile" -> { - val text: String? = call.argument("text") - if (synth) { - result.success(0) - return - } - val fileName: String? = call.argument("fileName") - val isFullPath: Boolean? = call.argument("isFullPath") - synthesizeToFile(text!!, fileName!!, isFullPath!!) - if (awaitSynthCompletion) { - synth = true - synthResult = result - } else { - result.success(1) - } - } - - "pause" -> { - isPaused = true - if (pauseText != null) { - pauseText = pauseText!!.substring(lastProgress) - } - stop() - result.success(1) - if (speakResult != null) { - speakResult!!.success(0) - speakResult = null - } - } - - "stop" -> { - isPaused = false - pauseText = null - stop() - lastProgress = 0 - result.success(1) - if (speakResult != null) { - speakResult!!.success(0) - speakResult = null - } - } - - "setEngine" -> { - val engine: String = call.arguments.toString() - setEngine(engine, result) - } - - "setSpeechRate" -> { - val rate: String = call.arguments.toString() - // To make the FlutterTts API consistent across platforms, - // Android 1.0 is mapped to flutter 0.5. - setSpeechRate(rate.toFloat() * 2.0f) - result.success(1) - } - - "setVolume" -> { - val volume: String = call.arguments.toString() - setVolume(volume.toFloat(), result) - } - - "setPitch" -> { - val pitch: String = call.arguments.toString() - setPitch(pitch.toFloat(), result) - } - - "setLanguage" -> { - val language: String = call.arguments.toString() - setLanguage(language, result) - } - - "getLanguages" -> getLanguages(result) - "getVoices" -> getVoices(result) - "getSpeechRateValidRange" -> getSpeechRateValidRange(result) - "getEngines" -> getEngines(result) - "getDefaultEngine" -> getDefaultEngine(result) - "getDefaultVoice" -> getDefaultVoice(result) - "setVoice" -> { - val voice: HashMap? = call.arguments() - setVoice(voice!!, result) - } - - "clearVoice" -> clearVoice(result) - - "isLanguageAvailable" -> { - val language: String = call.arguments.toString() - val locale: Locale = Locale.forLanguageTag(language) - result.success(isLanguageAvailable(locale)) - } - - "setSilence" -> { - val silencems: String = call.arguments.toString() - this.silencems = silencems.toInt() - } - - "setSharedInstance" -> result.success(1) - "isLanguageInstalled" -> { - val language: String = call.arguments.toString() - result.success(isLanguageInstalled(language)) - } - - "areLanguagesInstalled" -> { - val languages: List? = call.arguments() - result.success(areLanguagesInstalled(languages!!)) - } - - "setQueueMode" -> { - val queueMode: String = call.arguments.toString() - this.queueMode = queueMode.toInt() - result.success(1) - } - - "setAudioAttributesForNavigation" -> { - setAudioAttributesForNavigation() - result.success(1) - } - - else -> result.notImplemented() - } - } - - private fun setSpeechRate(rate: Float) { + private fun setSpeechRateImpl(rate: Float) { tts!!.setSpeechRate(rate) } - private fun isLanguageAvailable(locale: Locale?): Boolean { + private fun isLanguageAvailableImpl(locale: Locale?): Boolean { return tts!!.isLanguageAvailable(locale) >= TextToSpeech.LANG_AVAILABLE } - private fun areLanguagesInstalled(languages: List): Map { - val result: MutableMap = HashMap() + private fun areLanguagesInstalledImpl(languages: List): Map { + val result: MutableMap = HashMap() for (language in languages) { - result[language] = isLanguageInstalled(language) + result[language] = isLanguageInstalledImpl(language) } return result } - private fun isLanguageInstalled(language: String?): Boolean { + private fun isLanguageInstalledImpl(language: String?): Boolean { val locale: Locale = Locale.forLanguageTag(language!!) - if (isLanguageAvailable(locale)) { + if (isLanguageAvailableImpl(locale)) { var voiceToCheck: Voice? = null for (v in tts!!.voices) { if (v.locale == locale && !v.isNetworkConnectionRequired) { @@ -494,102 +329,84 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { return false } - private fun setEngine(engine: String?, result: Result) { + private fun setEngineImpl(engine: String?, result: ResultCallback) { ttsStatus = null selectedEngine = engine engineResult = result - tts = TextToSpeech(context, onInitListenerWithCallback, engine) - } - private fun setLanguage(language: String?, result: Result) { - val locale: Locale = Locale.forLanguageTag(language!!) - if (isLanguageAvailable(locale)) { - tts!!.language = locale - result.success(1) - } else { - result.success(0) - } + handler?.postDelayed(onInitTimeoutRunnable, kTtsInitTimeOutMs) + tts = TextToSpeech(context, onInitListenerWithCallback, engine) } - private fun setVoice(voice: HashMap, result: Result) { + private fun setVoiceImpl( + voice: com.tundralabs.fluttertts.Voice, callback: (Result) -> Unit + ) { for (ttsVoice in tts!!.voices) { - if (ttsVoice.name == voice["name"] && ttsVoice.locale - .toLanguageTag() == voice["locale"] - ) { + if (ttsVoice.name == voice.name && ttsVoice.locale.toLanguageTag() == voice.locale) { tts!!.voice = ttsVoice - result.success(1) + callback(Result.success(TtsResult(true))) return } } Log.d(tag, "Voice name not found: $voice") - result.success(0) + callback(Result.success(TtsResult(false))) } - private fun clearVoice(result: Result) { + private fun clearVoiceImpl(callback: ResultCallback) { tts!!.voice = tts!!.defaultVoice - result.success(1) + callback(Result.success(TtsResult(true))) } - private fun setVolume(volume: Float, result: Result) { + private fun setVolumeImpl(volume: Float, callback: ResultCallback) { if (volume in (0.0f..1.0f)) { bundle!!.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, volume) - result.success(1) + callback(Result.success(TtsResult(true))) } else { Log.d(tag, "Invalid volume $volume value - Range is from 0.0 to 1.0") - result.success(0) + callback(Result.success(TtsResult(false))) } } - private fun setPitch(pitch: Float, result: Result) { + private fun setPitchImpl(pitch: Float, callback: ResultCallback) { if (pitch in (0.5f..2.0f)) { tts!!.setPitch(pitch) - result.success(1) + callback(Result.success(TtsResult(true))) } else { Log.d(tag, "Invalid pitch $pitch value - Range is from 0.5 to 2.0") - result.success(0) + callback(Result.success(TtsResult(false))) } } - private fun getVoices(result: Result) { - val voices = ArrayList>() + private fun getVoicesImpl(result: ResultCallback>) { + val voices = ArrayList() try { for (voice in tts!!.voices) { - val voiceMap = HashMap() - readVoiceProperties(voiceMap, voice) - voices.add(voiceMap) + voices.add(readVoiceProperties(voice)) } - result.success(voices) + result(Result.success(voices)) } catch (e: NullPointerException) { Log.d(tag, "getVoices: " + e.message) - result.success(null) + result(Result.success(voices)) } } - private fun getLanguages(result: Result) { + private fun getLanguagesImpl(result: ResultCallback>) { val locales = ArrayList() try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // While this method was introduced in API level 21, it seems that it - // has not been implemented in the speech service side until API Level 23. - for (locale in tts!!.availableLanguages) { - locales.add(locale.toLanguageTag()) - } - } else { - for (locale in Locale.getAvailableLocales()) { - if (locale.variant.isEmpty() && isLanguageAvailable(locale)) { - locales.add(locale.toLanguageTag()) - } - } + // While this method was introduced in API level 21, it seems that it + // has not been implemented in the speech service side until API Level 23. + for (locale in tts!!.availableLanguages) { + locales.add(locale.toLanguageTag()) } } catch (e: MissingResourceException) { Log.d(tag, "getLanguages: " + e.message) } catch (e: NullPointerException) { Log.d(tag, "getLanguages: " + e.message) } - result.success(locales) + result(Result.success(locales)) } - private fun getEngines(result: Result) { + private fun getEnginesImpl(result: ResultCallback>) { val engines = ArrayList() try { for (engineInfo in tts!!.engines) { @@ -598,31 +415,28 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { } catch (e: Exception) { Log.d(tag, "getEngines: " + e.message) } - result.success(engines) + result(Result.success(engines)) } - private fun getDefaultEngine(result: Result) { + private fun getDefaultEngineImpl(result: ResultCallback) { val defaultEngine: String? = tts!!.defaultEngine - result.success(defaultEngine) + result(Result.success(defaultEngine)) } - private fun getDefaultVoice(result: Result) { + private fun getDefaultVoiceImpl(result: ResultCallback) { val defaultVoice: Voice? = tts!!.defaultVoice - val voice = HashMap() + var voice: com.tundralabs.fluttertts.Voice? = null if (defaultVoice != null) { - readVoiceProperties(voice, defaultVoice) + voice = readVoiceProperties(defaultVoice) } - result.success(voice) + result(Result.success(voice)) } + // Add voice properties into the voice map - fun readVoiceProperties(map: MutableMap, voice: Voice) { - map["name"] = voice.name - map["locale"] = voice.locale.toLanguageTag() - map["quality"] = qualityToString(voice.quality) - map["latency"] = latencyToString(voice.latency) - map["network_required"] = if (voice.isNetworkConnectionRequired) "1" else "0" - map["features"] = voice.features.joinToString(separator = "\t") - + fun readVoiceProperties(voice: Voice): com.tundralabs.fluttertts.Voice { + return Voice( + voice.name, voice.locale.toLanguageTag(), null, qualityToString(voice.quality), null + ) } // Function to map quality integer to the constant name @@ -636,44 +450,29 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { else -> "unknown" } } - // Function to map latency integer to the constant name - fun latencyToString(quality: Int): String { - return when (quality) { - Voice.LATENCY_VERY_HIGH -> "very high" - Voice.LATENCY_HIGH -> "high" - Voice.LATENCY_NORMAL -> "normal" - Voice.LATENCY_LOW -> "low" - Voice.LATENCY_VERY_LOW -> "very low" - else -> "unknown" - } - } - private fun getSpeechRateValidRange(result: Result) { + private fun getSpeechRateValidRangeImpl(result: ResultCallback) { // Valid values available in the android documentation. // https://developer.android.com/reference/android/speech/tts/TextToSpeech#setSpeechRate(float) // To make the FlutterTts API consistent across platforms, // we map Android 1.0 to flutter 0.5 and so on. - val data = HashMap() - data["min"] = "0" - data["normal"] = "0.5" - data["max"] = "1.5" - data["platform"] = "android" - result.success(data) + val data = TtsRateValidRange( + 0.0, 0.5, 1.5, TtsPlatform.ANDROID + ) + result(Result.success(data)) } - private fun speak(text: String, focus: Boolean): Boolean { + private fun speakImpl(text: String, focus: Boolean): Boolean { val uuid: String = UUID.randomUUID().toString() utterances[uuid] = text return if (ismServiceConnectionUsable(tts)) { - if(focus){ + if (focus) { requestAudioFocus() } - if (silencems > 0) { + if (silenceMs > 0) { tts!!.playSilentUtterance( - silencems.toLong(), - TextToSpeech.QUEUE_FLUSH, - SILENCE_PREFIX + uuid + silenceMs.toLong(), TextToSpeech.QUEUE_FLUSH, SILENCE_PREFIX + uuid ) tts!!.speak(text, TextToSpeech.QUEUE_ADD, bundle, uuid) == 0 } else { @@ -681,12 +480,13 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { } } else { ttsStatus = null + handler?.postDelayed(onInitTimeoutRunnable, kTtsInitTimeOutMs) tts = TextToSpeech(context, onInitListenerWithoutCallback, selectedEngine) false } } - private fun stop() { + private fun stopImpl() { if (awaitSynthCompletion) synth = false if (awaitSpeakCompletion) speaking = false tts!!.stop() @@ -695,8 +495,8 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { private val maxSpeechInputLength: Int get() = TextToSpeech.getMaxSpeechInputLength() - private fun closeParcelFileDescriptor(isError: Boolean) { - if (this.parcelFileDescriptor != null) { + private fun closeParcelFileDescriptor(isError: Boolean) { + if (this.parcelFileDescriptor != null) { if (isError) { this.parcelFileDescriptor!!.closeWithError("Error synthesizing TTS to file") } else { @@ -705,39 +505,40 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { } } - private fun synthesizeToFile(text: String, fileName: String, isFullPath: Boolean) { + private fun synthesizeToFileImpl(text: String, fileName: String, isFullPath: Boolean) { val fullPath: String val uuid: String = UUID.randomUUID().toString() bundle!!.putString( - TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, - SYNTHESIZE_TO_FILE_PREFIX + uuid + TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, SYNTHESIZE_TO_FILE_PREFIX + uuid ) - val result: Int = - if(isFullPath){ - val file = File(fileName) - fullPath = file.path - - tts!!.synthesizeToFile(text, bundle!!, file!!, SYNTHESIZE_TO_FILE_PREFIX + uuid) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val resolver = this.context?.contentResolver - val contentValues = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) - put(MediaStore.MediaColumns.MIME_TYPE, "audio/wav") - put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_MUSIC) - } - val uri = resolver?.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues) - this.parcelFileDescriptor = resolver?.openFileDescriptor(uri!!, "rw") - fullPath = uri?.path + File.separatorChar + fileName + val result: Int = if (isFullPath) { + val file = File(fileName) + fullPath = file.path + + tts!!.synthesizeToFile(text, bundle!!, file, SYNTHESIZE_TO_FILE_PREFIX + uuid) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val resolver = this.context?.contentResolver + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) + put(MediaStore.MediaColumns.MIME_TYPE, "audio/wav") + put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_MUSIC) + } + val uri = resolver?.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues) + this.parcelFileDescriptor = resolver?.openFileDescriptor(uri!!, "rw") + fullPath = uri?.path + File.separatorChar + fileName - tts!!.synthesizeToFile(text, bundle!!, parcelFileDescriptor!!, SYNTHESIZE_TO_FILE_PREFIX + uuid) - } else { - val musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC) - val file = File(musicDir, fileName) - fullPath = file.path + tts!!.synthesizeToFile( + text, bundle!!, parcelFileDescriptor!!, SYNTHESIZE_TO_FILE_PREFIX + uuid + ) + } else { + val musicDir = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC) + val file = File(musicDir, fileName) + fullPath = file.path - tts!!.synthesizeToFile(text, bundle!!, file!!, SYNTHESIZE_TO_FILE_PREFIX + uuid) - } + tts!!.synthesizeToFile(text, bundle!!, file, SYNTHESIZE_TO_FILE_PREFIX + uuid) + } if (result == TextToSpeech.SUCCESS) { Log.d(tag, "Successfully created file : $fullPath") @@ -746,15 +547,6 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { } } - private fun invokeMethod(method: String, arguments: Any) { - handler!!.post { - if (methodChannel != null) methodChannel!!.invokeMethod( - method, - arguments - ) - } - } - private fun ismServiceConnectionUsable(tts: TextToSpeech?): Boolean { var isBindConnection = true if (tts == null) { @@ -782,12 +574,11 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { } // Method to set AudioAttributes for navigation usage - private fun setAudioAttributesForNavigation() { + private fun setAudioAttributesForNavigationImpl() { if (tts != null) { val audioAttributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build() + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH).build() tts!!.setAudioAttributes(audioAttributes) } } @@ -796,12 +587,15 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { audioManager = context?.getSystemService(Context.AUDIO_SERVICE) as AudioManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) - .setOnAudioFocusChangeListener { /* opcional para monitorar mudanças de foco */ } - .build() + audioFocusRequest = + AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) + .setOnAudioFocusChangeListener { /* opcional para monitorar mudanças de foco */ } + .build() audioManager?.requestAudioFocus(audioFocusRequest!!) } else { - audioManager?.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) + audioManager?.requestAudioFocus( + null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + ) } } @@ -812,4 +606,475 @@ class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { audioManager?.abandonAudioFocus(null) } } + + override fun speak( + text: String, forceFocus: Boolean, callback: (Result) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { speak(text, forceFocus, callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback( + FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult() + ) + return + } + } + + if (pauseText == null) { + pauseText = text + currentText = pauseText!! + } + + if (isPaused) { + // Ensure the text hasn't changed + if (currentText != text) { + pauseText = text + currentText = pauseText!! + lastProgress = 0 + } + } + if (speaking) { + // If TTS is set to queue mode, allow the utterance to be queued up rather than discarded + if (queueMode == TextToSpeech.QUEUE_FLUSH) { + callback(Result.success(TtsResult(false))) + return + } + } + val b = speakImpl(text, forceFocus) + if (!b) { + synchronized(this@FlutterTtsPlugin) { + speak(text, forceFocus, callback) + } + return + } + // Only use await speak completion if queueMode is set to QUEUE_FLUSH + if (awaitSpeakCompletion && queueMode == TextToSpeech.QUEUE_FLUSH) { + speaking = true + speakResult = callback + } else { + callback(Result.success(TtsResult(true))) + } + + } + + override fun pause(callback: (Result) -> Unit) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { pause(callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback( + FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult() + ) + return + } + } + + isPaused = true + if (pauseText != null) { + pauseText = pauseText!!.substring(lastProgress) + } + stopImpl() + callback(Result.success(TtsResult(true))) + if (speakResult != null) { + speakResult?.invoke(Result.success(TtsResult(false))) + speakResult = null + } + } + + override fun stop(callback: (Result) -> Unit) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { stop(callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback( + FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult() + ) + return + } + } + + isPaused = false + pauseText = null + stopImpl() + lastProgress = 0 + callback(Result.success(TtsResult(true))) + if (speakResult != null) { + speakResult?.invoke(Result.success(TtsResult(false))) + speakResult = null + } + } + + override fun setSpeechRate( + rate: Double, callback: (Result) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { setSpeechRate(rate, callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + // To make the FlutterTts API consistent across platforms, + // Android 1.0 is mapped to flutter 0.5. + setSpeechRateImpl(rate.toFloat() * 2.0f) + callback(Result.success(TtsResult(true))) + } + + override fun setVolume( + volume: Double, callback: (Result) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { setVolume(volume, callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + setVolumeImpl(volume.toFloat(), callback) + } + + override fun setPitch( + pitch: Double, callback: (Result) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { setPitch(pitch, callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + setPitchImpl(pitch.toFloat(), callback) + } + + override fun setVoice( + voice: com.tundralabs.fluttertts.Voice, callback: (Result) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { setVoice(voice, callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + setVoiceImpl(voice, callback) + } + + override fun clearVoice(callback: (Result) -> Unit) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { clearVoice(callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + clearVoiceImpl(callback) + } + + override fun awaitSpeakCompletion( + awaitCompletion: Boolean, callback: (Result) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { awaitSpeakCompletion(awaitCompletion, callback); } + pendingMethodCalls.add(suspendedCall) + return + } + } + + awaitSpeakCompletion = awaitCompletion + callback(Result.success(TtsResult(true))) + } + + override fun getLanguages(callback: (Result>) -> Unit) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { getLanguages(callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + getLanguagesImpl(callback) + } + + override fun getVoices(callback: (Result>) -> Unit) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { getVoices(callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + getVoicesImpl(callback) + } + + override fun awaitSynthCompletion( + awaitCompletion: Boolean, callback: (Result) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { awaitSynthCompletion(awaitCompletion, callback); } + pendingMethodCalls.add(suspendedCall) + return + } + } + + awaitSynthCompletion = awaitCompletion + callback(Result.success(TtsResult(true))) + } + + override fun getMaxSpeechInputLength(callback: (Result) -> Unit) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { getMaxSpeechInputLength(callback); } + pendingMethodCalls.add(suspendedCall) + return + } + } + + callback(Result.success(maxSpeechInputLength.toLong())) + } + + override fun setEngine( + engine: String, callback: (Result) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { setEngine(engine, callback); } + pendingMethodCalls.add(suspendedCall) + return + } + } + + setEngineImpl(engine, callback) + } + + override fun getEngines(callback: (Result>) -> Unit) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { getEngines(callback); } + pendingMethodCalls.add(suspendedCall) + return + } + } + + getEnginesImpl(callback) + } + + override fun getDefaultEngine(callback: (Result) -> Unit) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { getDefaultEngine(callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + getDefaultEngineImpl(callback) + } + + override fun getDefaultVoice(callback: (Result) -> Unit) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { getDefaultVoice(callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + getDefaultVoiceImpl(callback) + } + + override fun synthesizeToFile( + text: String, fileName: String, isFullPath: Boolean, callback: (Result) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = + Runnable { synthesizeToFile(text, fileName, isFullPath, callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + if (synth) { + callback(Result.success(TtsResult(false))) + return + } + synthesizeToFileImpl(text, fileName, isFullPath) + if (awaitSynthCompletion) { + synth = true + synthResult = callback + } else { + callback(Result.success(TtsResult(true))) + } + } + + override fun isLanguageInstalled( + language: String, callback: (Result) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { isLanguageInstalled(language, callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + callback(Result.success(isLanguageInstalledImpl(language))) + } + + override fun isLanguageAvailable( + language: String, callback: (Result) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { isLanguageAvailable(language, callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + val locale: Locale = Locale.forLanguageTag(language) + callback(Result.success(isLanguageAvailableImpl(locale))) + } + + override fun areLanguagesInstalled( + languages: List, callback: (Result>) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { areLanguagesInstalled(languages, callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + callback(Result.success(areLanguagesInstalledImpl(languages))) + } + + override fun getSpeechRateValidRange(callback: (Result) -> Unit) { + getSpeechRateValidRangeImpl(callback) + } + + override fun setSilence( + timems: Long, callback: (Result) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { setSilence(timems, callback); } + pendingMethodCalls.add(suspendedCall) + return + } + } + + this.silenceMs = timems.toInt() + } + + override fun setQueueMode( + queueMode: Long, callback: (Result) -> Unit + ) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { setQueueMode(queueMode, callback); } + pendingMethodCalls.add(suspendedCall) + return + } + } + + this.queueMode = queueMode.toInt() + callback(Result.success(TtsResult(true))) + } + + override fun setAudioAttributesForNavigation(callback: (Result) -> Unit) { + synchronized(this@FlutterTtsPlugin) { + if (ttsStatus == null) { + // Suspend method call until the TTS engine is ready + val suspendedCall = Runnable { setAudioAttributesForNavigation(callback); } + pendingMethodCalls.add(suspendedCall) + return + } else if (ttsStatus != TextToSpeech.SUCCESS) { + callback(FlutterTtsErrorCode.TTS_NOT_AVAILABLE.toKtResult()) + return + } + } + + setAudioAttributesForNavigationImpl() + callback(Result.success(TtsResult(true))) + } } diff --git a/android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt b/android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt new file mode 100644 index 00000000..228ef07b --- /dev/null +++ b/android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt @@ -0,0 +1,1670 @@ +// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package com.tundralabs.fluttertts + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object MessagesPigeonUtils { + + fun createConnectionError(channelName: String): FlutterError { + return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") } + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + return a.contentEquals(b) + } + if (a is Array<*> && b is Array<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is List<*> && b is List<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is Map<*, *> && b is Map<*, *>) { + return a.size == b.size && a.all { + (b as Map).contains(it.key) && + deepEquals(it.value, b[it.key]) + } + } + return a == b + } + +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +enum class FlutterTtsErrorCode(val raw: Int) { + /** general error code for TTS engine not available. */ + TTS_NOT_AVAILABLE(0), + /** + * The TTS engine failed to initialize in n second. + * 1 second is the default timeout. + * e.g. Some Android custom ROMS may trim TTS service, + * and third party TTS engine may fail to initialize due to battery optimization. + */ + TTS_INIT_TIMEOUT(1), + /** not supported on current os version */ + NOT_SUPPORTED_OSVERSION(2); + + companion object { + fun ofRaw(raw: Int): FlutterTtsErrorCode? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** + * Audio session category identifiers for iOS. + * + * See also: + * * https://developer.apple.com/documentation/avfaudio/avaudiosession/category + */ +enum class IosTextToSpeechAudioCategory(val raw: Int) { + /** + * The default audio session category. + * + * Your audio is silenced by screen locking and by the Silent switch. + * + * By default, using this category implies that your app’s audio + * is nonmixable—activating your session will interrupt + * any other audio sessions which are also nonmixable. + * To allow mixing, use the [ambient] category instead. + */ + AMBIENT_SOLO(0), + /** + * The category for an app in which sound playback is nonprimary — that is, + * your app also works with the sound turned off. + * + * This category is also appropriate for “play-along” apps, + * such as a virtual piano that a user plays while the Music app is playing. + * When you use this category, audio from other apps mixes with your audio. + * Screen locking and the Silent switch (on iPhone, the Ring/Silent switch) silence your audio. + */ + AMBIENT(1), + /** + * The category for playing recorded music or other sounds + * that are central to the successful use of your app. + * + * When using this category, your app audio continues + * with the Silent switch set to silent or when the screen locks. + * + * By default, using this category implies that your app’s audio + * is nonmixable—activating your session will interrupt + * any other audio sessions which are also nonmixable. + * To allow mixing for this category, use the + * [IosTextToSpeechAudioCategoryOptions.mixWithOthers] option. + */ + PLAYBACK(2), + /** + * The category for recording (input) and playback (output) of audio, + * such as for a Voice over Internet Protocol (VoIP) app. + * + * Your audio continues with the Silent switch set to silent and with the screen locked. + * This category is appropriate for simultaneous recording and playback, + * and also for apps that record and play back, but not simultaneously. + */ + PLAY_AND_RECORD(3); + + companion object { + fun ofRaw(raw: Int): IosTextToSpeechAudioCategory? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** + * Audio session mode identifiers for iOS. + * + * See also: + * * https://developer.apple.com/documentation/avfaudio/avaudiosession/mode + */ +enum class IosTextToSpeechAudioMode(val raw: Int) { + /** + * The default audio session mode. + * + * You can use this mode with every [IosTextToSpeechAudioCategory]. + */ + DEFAULT_MODE(0), + /** + * A mode that the GameKit framework sets on behalf of an application + * that uses GameKit’s voice chat service. + * + * This mode is valid only with the + * [IosTextToSpeechAudioCategory.playAndRecord] category. + * + * Don’t set this mode directly. If you need similar behavior and aren’t + * using a `GKVoiceChat` object, use [voiceChat] or [videoChat] instead. + */ + GAME_CHAT(1), + /** + * A mode that indicates that your app is performing measurement of audio input or output. + * + * Use this mode for apps that need to minimize the amount of + * system-supplied signal processing to input and output signals. + * If recording on devices with more than one built-in microphone, + * the session uses the primary microphone. + * + * For use with the [IosTextToSpeechAudioCategory.playback] or + * [IosTextToSpeechAudioCategory.playAndRecord] category. + * + * **Important:** This mode disables some dynamics processing on input and output signals, + * resulting in a lower-output playback level. + */ + MEASUREMENT(2), + /** + * A mode that indicates that your app is playing back movie content. + * + * When you set this mode, the audio session uses signal processing to enhance + * movie playback for certain audio routes such as built-in speaker or headphones. + * You may only use this mode with the + * [IosTextToSpeechAudioCategory.playback] category. + */ + MOVIE_PLAYBACK(3), + /** + * A mode used for continuous spoken audio to pause the audio when another app plays a short audio prompt. + * + * This mode is appropriate for apps that play continuous spoken audio, + * such as podcasts or audio books. Setting this mode indicates that your app + * should pause, rather than duck, its audio if another app plays + * a spoken audio prompt. After the interrupting app’s audio ends, you can + * resume your app’s audio playback. + */ + SPOKEN_AUDIO(4), + /** + * A mode that indicates that your app is engaging in online video conferencing. + * + * Use this mode for video chat apps that use the + * [IosTextToSpeechAudioCategory.playAndRecord] category. + * When you set this mode, the audio session optimizes the device’s tonal + * equalization for voice. It also reduces the set of allowable audio routes + * to only those appropriate for video chat. + * + * Using this mode has the side effect of enabling the + * [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. + */ + VIDEO_CHAT(5), + /** + * A mode that indicates that your app is recording a movie. + * + * This mode is valid only with the + * [IosTextToSpeechAudioCategory.playAndRecord] category. + * On devices with more than one built-in microphone, + * the audio session uses the microphone closest to the video camera. + * + * Use this mode to ensure that the system provides appropriate audio-signal processing. + */ + VIDEO_RECORDING(6), + /** + * A mode that indicates that your app is performing two-way voice communication, + * such as using Voice over Internet Protocol (VoIP). + * + * Use this mode for Voice over IP (VoIP) apps that use the + * [IosTextToSpeechAudioCategory.playAndRecord] category. + * When you set this mode, the session optimizes the device’s tonal + * equalization for voice and reduces the set of allowable audio routes + * to only those appropriate for voice chat. + * + * Using this mode has the side effect of enabling the + * [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. + */ + VOICE_CHAT(7), + /** + * A mode that indicates that your app plays audio using text-to-speech. + * + * Setting this mode allows for different routing behaviors when your app + * is connected to certain audio devices, such as CarPlay. + * An example of an app that uses this mode is a turn-by-turn navigation app + * that plays short prompts to the user. + * + * Typically, apps of the same type also configure their sessions to use the + * [IosTextToSpeechAudioCategoryOptions.duckOthers] and + * [IosTextToSpeechAudioCategoryOptions.interruptSpokenAudioAndMixWithOthers] options. + */ + VOICE_PROMPT(8); + + companion object { + fun ofRaw(raw: Int): IosTextToSpeechAudioMode? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** + * Audio session category options for iOS. + * + * See also: + * * https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions + */ +enum class IosTextToSpeechAudioCategoryOptions(val raw: Int) { + /** + * An option that indicates whether audio from this session mixes with audio + * from active sessions in other audio apps. + * + * You can set this option explicitly only if the audio session category + * is [IosTextToSpeechAudioCategory.playAndRecord] or + * [IosTextToSpeechAudioCategory.playback]. + * If you set the audio session category to [IosTextToSpeechAudioCategory.ambient], + * the session automatically sets this option. + * Likewise, setting the [duckOthers] or [interruptSpokenAudioAndMixWithOthers] + * options also enables this option. + * + * If you set this option, your app mixes its audio with audio playing + * in background apps, such as the Music app. + */ + MIX_WITH_OTHERS(0), + /** + * An option that reduces the volume of other audio sessions while audio + * from this session plays. + * + * You can set this option only if the audio session category is + * [IosTextToSpeechAudioCategory.playAndRecord] or + * [IosTextToSpeechAudioCategory.playback]. + * Setting it implicitly sets the [mixWithOthers] option. + * + * Use this option to mix your app’s audio with that of others. + * While your app plays its audio, the system reduces the volume of other + * audio sessions to make yours more prominent. If your app provides + * occasional spoken audio, such as in a turn-by-turn navigation app + * or an exercise app, you should also set the [interruptSpokenAudioAndMixWithOthers] option. + * + * Note that ducking begins when you activate your app’s audio session + * and ends when you deactivate the session. + * + * See also: + * * [FlutterTts.setSharedInstance] + */ + DUCK_OTHERS(1), + /** + * An option that determines whether to pause spoken audio content + * from other sessions when your app plays its audio. + * + * You can set this option only if the audio session category is + * [IosTextToSpeechAudioCategory.playAndRecord] or + * [IosTextToSpeechAudioCategory.playback]. + * Setting this option also sets [mixWithOthers]. + * + * If you set this option, the system mixes your audio with other + * audio sessions, but interrupts (and stops) audio sessions that use the + * [IosTextToSpeechAudioMode.spokenAudio] audio session mode. + * It pauses the audio from other apps as long as your session is active. + * After your audio session deactivates, the system resumes the interrupted app’s audio. + * + * Set this option if your app’s audio is occasional and spoken, + * such as in a turn-by-turn navigation app or an exercise app. + * This avoids intelligibility problems when two spoken audio apps mix. + * If you set this option, also set the [duckOthers] option unless + * you have a specific reason not to. Ducking other audio, rather than + * interrupting it, is appropriate when the other audio isn’t spoken audio. + */ + INTERRUPT_SPOKEN_AUDIO_AND_MIX_WITH_OTHERS(2), + /** + * An option that determines whether Bluetooth hands-free devices appear + * as available input routes. + * + * You can set this option only if the audio session category is + * [IosTextToSpeechAudioCategory.playAndRecord] or + * [IosTextToSpeechAudioCategory.playback]. + * + * You’re required to set this option to allow routing audio input and output + * to a paired Bluetooth Hands-Free Profile (HFP) device. + * If you clear this option, paired Bluetooth HFP devices don’t show up + * as available audio input routes. + */ + ALLOW_BLUETOOTH(3), + /** + * An option that determines whether you can stream audio from this session + * to Bluetooth devices that support the Advanced Audio Distribution Profile (A2DP). + * + * A2DP is a stereo, output-only profile intended for higher bandwidth + * audio use cases, such as music playback. + * The system automatically routes to A2DP ports if you configure an + * app’s audio session to use the [IosTextToSpeechAudioCategory.ambient], + * [IosTextToSpeechAudioCategory.ambientSolo], or + * [IosTextToSpeechAudioCategory.playback] categories. + * + * Starting with iOS 10.0, apps using the + * [IosTextToSpeechAudioCategory.playAndRecord] category may also allow + * routing output to paired Bluetooth A2DP devices. To enable this behavior, + * pass this category option when setting your audio session’s category. + * + * Note: If this option and the [allowBluetooth] option are both set, + * when a single device supports both the Hands-Free Profile (HFP) and A2DP, + * the system gives hands-free ports a higher priority for routing. + */ + ALLOW_BLUETOOTH_A2DP(4), + /** + * An option that determines whether you can stream audio + * from this session to AirPlay devices. + * + * Setting this option enables the audio session to route audio output + * to AirPlay devices. You can only explicitly set this option if the + * audio session’s category is set to [IosTextToSpeechAudioCategory.playAndRecord]. + * For most other audio session categories, the system sets this option implicitly. + */ + ALLOW_AIR_PLAY(5), + /** + * An option that determines whether audio from the session defaults to the built-in speaker instead of the receiver. + * + * You can set this option only when using the + * [IosTextToSpeechAudioCategory.playAndRecord] category. + * It’s used to modify the category’s routing behavior so that audio + * is always routed to the speaker rather than the receiver if + * no other accessories, such as headphones, are in use. + * + * When using this option, the system honors user gestures. + * For example, plugging in a headset causes the route to change to + * headset mic/headphones, and unplugging the headset causes the route + * to change to built-in mic/speaker (as opposed to built-in mic/receiver) + * when you’ve set this override. + * + * In the case of using a USB input-only accessory, audio input + * comes from the accessory, and the system routes audio to the headphones, + * if attached, or to the speaker if the headphones aren’t plugged in. + * The use case is to route audio to the speaker instead of the receiver + * in cases where the audio would normally go to the receiver. + */ + DEFAULT_TO_SPEAKER(6); + + companion object { + fun ofRaw(raw: Int): IosTextToSpeechAudioCategoryOptions? { + return values().firstOrNull { it.raw == raw } + } + } +} + +enum class TtsPlatform(val raw: Int) { + ANDROID(0), + IOS(1); + + companion object { + fun ofRaw(raw: Int): TtsPlatform? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class Voice ( + val name: String, + val locale: String, + val gender: String? = null, + val quality: String? = null, + val identifier: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): Voice { + val name = pigeonVar_list[0] as String + val locale = pigeonVar_list[1] as String + val gender = pigeonVar_list[2] as String? + val quality = pigeonVar_list[3] as String? + val identifier = pigeonVar_list[4] as String? + return Voice(name, locale, gender, quality, identifier) + } + } + fun toList(): List { + return listOf( + name, + locale, + gender, + quality, + identifier, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is Voice) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class TtsResult ( + val success: Boolean, + val message: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): TtsResult { + val success = pigeonVar_list[0] as Boolean + val message = pigeonVar_list[1] as String? + return TtsResult(success, message) + } + } + fun toList(): List { + return listOf( + success, + message, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is TtsResult) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class TtsProgress ( + val text: String, + val start: Long, + val end: Long, + val word: String +) + { + companion object { + fun fromList(pigeonVar_list: List): TtsProgress { + val text = pigeonVar_list[0] as String + val start = pigeonVar_list[1] as Long + val end = pigeonVar_list[2] as Long + val word = pigeonVar_list[3] as String + return TtsProgress(text, start, end, word) + } + } + fun toList(): List { + return listOf( + text, + start, + end, + word, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is TtsProgress) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class TtsRateValidRange ( + val minimum: Double, + val normal: Double, + val maximum: Double, + val platform: TtsPlatform +) + { + companion object { + fun fromList(pigeonVar_list: List): TtsRateValidRange { + val minimum = pigeonVar_list[0] as Double + val normal = pigeonVar_list[1] as Double + val maximum = pigeonVar_list[2] as Double + val platform = pigeonVar_list[3] as TtsPlatform + return TtsRateValidRange(minimum, normal, maximum, platform) + } + } + fun toList(): List { + return listOf( + minimum, + normal, + maximum, + platform, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is TtsRateValidRange) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} +private open class messagesPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { + FlutterTtsErrorCode.ofRaw(it.toInt()) + } + } + 130.toByte() -> { + return (readValue(buffer) as Long?)?.let { + IosTextToSpeechAudioCategory.ofRaw(it.toInt()) + } + } + 131.toByte() -> { + return (readValue(buffer) as Long?)?.let { + IosTextToSpeechAudioMode.ofRaw(it.toInt()) + } + } + 132.toByte() -> { + return (readValue(buffer) as Long?)?.let { + IosTextToSpeechAudioCategoryOptions.ofRaw(it.toInt()) + } + } + 133.toByte() -> { + return (readValue(buffer) as Long?)?.let { + TtsPlatform.ofRaw(it.toInt()) + } + } + 134.toByte() -> { + return (readValue(buffer) as? List)?.let { + Voice.fromList(it) + } + } + 135.toByte() -> { + return (readValue(buffer) as? List)?.let { + TtsResult.fromList(it) + } + } + 136.toByte() -> { + return (readValue(buffer) as? List)?.let { + TtsProgress.fromList(it) + } + } + 137.toByte() -> { + return (readValue(buffer) as? List)?.let { + TtsRateValidRange.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is FlutterTtsErrorCode -> { + stream.write(129) + writeValue(stream, value.raw.toLong()) + } + is IosTextToSpeechAudioCategory -> { + stream.write(130) + writeValue(stream, value.raw.toLong()) + } + is IosTextToSpeechAudioMode -> { + stream.write(131) + writeValue(stream, value.raw.toLong()) + } + is IosTextToSpeechAudioCategoryOptions -> { + stream.write(132) + writeValue(stream, value.raw.toLong()) + } + is TtsPlatform -> { + stream.write(133) + writeValue(stream, value.raw.toLong()) + } + is Voice -> { + stream.write(134) + writeValue(stream, value.toList()) + } + is TtsResult -> { + stream.write(135) + writeValue(stream, value.toList()) + } + is TtsProgress -> { + stream.write(136) + writeValue(stream, value.toList()) + } + is TtsRateValidRange -> { + stream.write(137) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface TtsHostApi { + fun speak(text: String, forceFocus: Boolean, callback: (Result) -> Unit) + fun pause(callback: (Result) -> Unit) + fun stop(callback: (Result) -> Unit) + fun setSpeechRate(rate: Double, callback: (Result) -> Unit) + fun setVolume(volume: Double, callback: (Result) -> Unit) + fun setPitch(pitch: Double, callback: (Result) -> Unit) + fun setVoice(voice: Voice, callback: (Result) -> Unit) + fun clearVoice(callback: (Result) -> Unit) + fun awaitSpeakCompletion(awaitCompletion: Boolean, callback: (Result) -> Unit) + fun getLanguages(callback: (Result>) -> Unit) + fun getVoices(callback: (Result>) -> Unit) + + companion object { + /** The codec used by TtsHostApi. */ + val codec: MessageCodec by lazy { + messagesPigeonCodec() + } + /** Sets up an instance of `TtsHostApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: TtsHostApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.speak$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val textArg = args[0] as String + val forceFocusArg = args[1] as Boolean + api.speak(textArg, forceFocusArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.pause$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.pause{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.stop$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.stop{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.setSpeechRate$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val rateArg = args[0] as Double + api.setSpeechRate(rateArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.setVolume$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val volumeArg = args[0] as Double + api.setVolume(volumeArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.setPitch$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val pitchArg = args[0] as Double + api.setPitch(pitchArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.setVoice$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val voiceArg = args[0] as Voice + api.setVoice(voiceArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.clearVoice$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.clearVoice{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.awaitSpeakCompletion$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val awaitCompletionArg = args[0] as Boolean + api.awaitSpeakCompletion(awaitCompletionArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.getLanguages$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.getLanguages{ result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.getVoices$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.getVoices{ result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface IosTtsHostApi { + fun awaitSynthCompletion(awaitCompletion: Boolean, callback: (Result) -> Unit) + fun synthesizeToFile(text: String, fileName: String, isFullPath: Boolean, callback: (Result) -> Unit) + fun setSharedInstance(sharedSession: Boolean, callback: (Result) -> Unit) + fun autoStopSharedSession(autoStop: Boolean, callback: (Result) -> Unit) + fun setIosAudioCategory(category: IosTextToSpeechAudioCategory, options: List, mode: IosTextToSpeechAudioMode, callback: (Result) -> Unit) + fun getSpeechRateValidRange(callback: (Result) -> Unit) + fun isLanguageAvailable(language: String, callback: (Result) -> Unit) + fun setLanguange(language: String, callback: (Result) -> Unit) + + companion object { + /** The codec used by IosTtsHostApi. */ + val codec: MessageCodec by lazy { + messagesPigeonCodec() + } + /** Sets up an instance of `IosTtsHostApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: IosTtsHostApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.awaitSynthCompletion$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val awaitCompletionArg = args[0] as Boolean + api.awaitSynthCompletion(awaitCompletionArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.synthesizeToFile$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val textArg = args[0] as String + val fileNameArg = args[1] as String + val isFullPathArg = args[2] as Boolean + api.synthesizeToFile(textArg, fileNameArg, isFullPathArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setSharedInstance$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val sharedSessionArg = args[0] as Boolean + api.setSharedInstance(sharedSessionArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.autoStopSharedSession$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val autoStopArg = args[0] as Boolean + api.autoStopSharedSession(autoStopArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setIosAudioCategory$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val categoryArg = args[0] as IosTextToSpeechAudioCategory + val optionsArg = args[1] as List + val modeArg = args[2] as IosTextToSpeechAudioMode + api.setIosAudioCategory(categoryArg, optionsArg, modeArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.getSpeechRateValidRange$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.getSpeechRateValidRange{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.isLanguageAvailable$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val languageArg = args[0] as String + api.isLanguageAvailable(languageArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setLanguange$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val languageArg = args[0] as String + api.setLanguange(languageArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface AndroidTtsHostApi { + fun awaitSynthCompletion(awaitCompletion: Boolean, callback: (Result) -> Unit) + fun getMaxSpeechInputLength(callback: (Result) -> Unit) + fun setEngine(engine: String, callback: (Result) -> Unit) + fun getEngines(callback: (Result>) -> Unit) + fun getDefaultEngine(callback: (Result) -> Unit) + fun getDefaultVoice(callback: (Result) -> Unit) + /** [Future] which invokes the platform specific method for synthesizeToFile */ + fun synthesizeToFile(text: String, fileName: String, isFullPath: Boolean, callback: (Result) -> Unit) + fun isLanguageInstalled(language: String, callback: (Result) -> Unit) + fun isLanguageAvailable(language: String, callback: (Result) -> Unit) + fun areLanguagesInstalled(languages: List, callback: (Result>) -> Unit) + fun getSpeechRateValidRange(callback: (Result) -> Unit) + fun setSilence(timems: Long, callback: (Result) -> Unit) + fun setQueueMode(queueMode: Long, callback: (Result) -> Unit) + fun setAudioAttributesForNavigation(callback: (Result) -> Unit) + + companion object { + /** The codec used by AndroidTtsHostApi. */ + val codec: MessageCodec by lazy { + messagesPigeonCodec() + } + /** Sets up an instance of `AndroidTtsHostApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: AndroidTtsHostApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.awaitSynthCompletion$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val awaitCompletionArg = args[0] as Boolean + api.awaitSynthCompletion(awaitCompletionArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getMaxSpeechInputLength$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.getMaxSpeechInputLength{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setEngine$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val engineArg = args[0] as String + api.setEngine(engineArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getEngines$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.getEngines{ result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getDefaultEngine$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.getDefaultEngine{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getDefaultVoice$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.getDefaultVoice{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.synthesizeToFile$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val textArg = args[0] as String + val fileNameArg = args[1] as String + val isFullPathArg = args[2] as Boolean + api.synthesizeToFile(textArg, fileNameArg, isFullPathArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.isLanguageInstalled$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val languageArg = args[0] as String + api.isLanguageInstalled(languageArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.isLanguageAvailable$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val languageArg = args[0] as String + api.isLanguageAvailable(languageArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.areLanguagesInstalled$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val languagesArg = args[0] as List + api.areLanguagesInstalled(languagesArg) { result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getSpeechRateValidRange$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.getSpeechRateValidRange{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setSilence$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val timemsArg = args[0] as Long + api.setSilence(timemsArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setQueueMode$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val queueModeArg = args[0] as Long + api.setQueueMode(queueModeArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setAudioAttributesForNavigation$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.setAudioAttributesForNavigation{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface MacosTtsHostApi { + fun awaitSynthCompletion(awaitCompletion: Boolean, callback: (Result) -> Unit) + fun getSpeechRateValidRange(callback: (Result) -> Unit) + fun setLanguange(language: String, callback: (Result) -> Unit) + fun isLanguageAvailable(language: String, callback: (Result) -> Unit) + + companion object { + /** The codec used by MacosTtsHostApi. */ + val codec: MessageCodec by lazy { + messagesPigeonCodec() + } + /** Sets up an instance of `MacosTtsHostApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: MacosTtsHostApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.awaitSynthCompletion$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val awaitCompletionArg = args[0] as Boolean + api.awaitSynthCompletion(awaitCompletionArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.getSpeechRateValidRange$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.getSpeechRateValidRange{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.setLanguange$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val languageArg = args[0] as String + api.setLanguange(languageArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.isLanguageAvailable$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val languageArg = args[0] as String + api.isLanguageAvailable(languageArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ +class TtsFlutterApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { + companion object { + /** The codec used by TtsFlutterApi. */ + val codec: MessageCodec by lazy { + messagesPigeonCodec() + } + } + fun onSpeakStartCb(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakStartCb$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName))) + } + } + } + fun onSpeakCompleteCb(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakCompleteCb$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName))) + } + } + } + fun onSpeakPauseCb(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakPauseCb$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName))) + } + } + } + fun onSpeakResumeCb(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakResumeCb$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName))) + } + } + } + fun onSpeakCancelCb(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakCancelCb$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName))) + } + } + } + fun onSpeakProgressCb(progressArg: TtsProgress, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakProgressCb$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(progressArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName))) + } + } + } + fun onSpeakErrorCb(errorArg: String, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakErrorCb$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(errorArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName))) + } + } + } + fun onSynthStartCb(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthStartCb$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName))) + } + } + } + fun onSynthCompleteCb(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthCompleteCb$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName))) + } + } + } + fun onSynthErrorCb(errorArg: String, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthErrorCb$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(errorArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName))) + } + } + } +} diff --git a/build.yaml b/build.yaml new file mode 100644 index 00000000..95e10778 --- /dev/null +++ b/build.yaml @@ -0,0 +1,2 @@ +additional_public_assets: + - pigeons/** diff --git a/example/.metadata b/example/.metadata index 3c0dd2c8..b9fd747a 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,5 +4,27 @@ # This file should be version controlled and should not be manually edited. version: - revision: b397406561f5e7a9c94e28f58d9e49fca0dd58b7 - channel: beta + revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: ios + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle deleted file mode 100644 index 4586c053..00000000 --- a/example/android/app/build.gradle +++ /dev/null @@ -1,49 +0,0 @@ -plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -android { - compileSdk 34 - namespace 'com.tundralabs.example' - - - defaultConfig { - applicationId "com.tundralabs.flutterttsexample" - minSdkVersion 21 - targetSdkVersion 34 - versionCode 1 - versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } - lint { - disable 'InvalidPackage' - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' -} diff --git a/example/android/app/build.gradle.kts b/example/android/app/build.gradle.kts new file mode 100644 index 00000000..92ff5830 --- /dev/null +++ b/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.tundralabs.example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.tundralabs.flutterttsexample" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/example/android/build.gradle b/example/android/build.gradle deleted file mode 100644 index bc157bd1..00000000 --- a/example/android/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/example/android/build.gradle.kts b/example/android/build.gradle.kts new file mode 100644 index 00000000..dbee657b --- /dev/null +++ b/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 022f0d32..f018a618 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,6 +1,3 @@ -android.defaults.buildfeatures.buildconfig=true -android.enableJetifier=true -android.nonFinalResIds=false -android.nonTransitiveRClass=false +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -org.gradle.jvmargs=-Xmx1536M +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 10e94ad6..26677571 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip +# distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.13-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle deleted file mode 100644 index 60906601..00000000 --- a/example/android/settings.gradle +++ /dev/null @@ -1,25 +0,0 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - }() - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.2.0" apply false - id "org.jetbrains.kotlin.android" version "1.9.10" apply false -} - -include ":app" \ No newline at end of file diff --git a/example/android/settings.gradle.kts b/example/android/settings.gradle.kts new file mode 100644 index 00000000..f1ab4f39 --- /dev/null +++ b/example/android/settings.gradle.kts @@ -0,0 +1,28 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + maven { url = uri("https://maven.aliyun.com/repository/google") } + maven { url = uri("https://maven.aliyun.com/repository/central") } + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.13.0" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/example/android/settings_aar.gradle b/example/android/settings_aar.gradle deleted file mode 100644 index e7b4def4..00000000 --- a/example/android/settings_aar.gradle +++ /dev/null @@ -1 +0,0 @@ -include ':app' diff --git a/example/ios/.gitignore b/example/ios/.gitignore index 1e1aafd6..7a7f9873 100644 --- a/example/ios/.gitignore +++ b/example/ios/.gitignore @@ -1,42 +1,34 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -*.pbxuser +**/dgph *.mode1v3 *.mode2v3 +*.moved-aside +*.pbxuser *.perspectivev3 - -!default.pbxuser +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. !default.mode1v3 !default.mode2v3 +!default.pbxuser !default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/app.flx -/Flutter/app.zip -/Flutter/flutter_assets/ -/Flutter/App.framework -/Flutter/Flutter.framework -/Flutter/Generated.xcconfig -/ServiceDefinitions.json - -Pods/ diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id deleted file mode 100644 index bfaaeba1..00000000 --- a/example/ios/Flutter/.last_build_id +++ /dev/null @@ -1 +0,0 @@ -b443c2d1b6b1e4df6c6fcc277a981410 \ No newline at end of file diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 9b41e7d8..1dc6cf76 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,11 +20,7 @@ ???? CFBundleVersion 1.0 - UIRequiredDeviceCapabilities - - arm64 - MinimumOSVersion - 11.0 + 13.0 diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index e8efba11..ec97fc6f 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index 399e9340..c4855bfe 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile index 88359b22..620e46eb 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -29,9 +29,11 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! - use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end end post_install do |installer| diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 38e2ecbc..7a9624c2 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,14 +8,26 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 59185AECC1AC289A18E08B27 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA1F5E9E308D75711360FAF6 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 936BDC305018E86F43152805 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B99FC96601B9D60C00E3472 /* Pods_RunnerTests.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - B9BC3BE22F97E1DD9B850D52 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B53E79FD7E80803BF54D70DB /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -30,10 +42,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 014F010A93253295F5620238 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 08484B4D3782F96E7B6281AA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 113E5C815831E8E83B1D24EE /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 7404F988052D3E92A73EBD7E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 4B99FC96601B9D60C00E3472 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -44,8 +61,10 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B53E79FD7E80803BF54D70DB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F597851B8230B83CB87D4CF3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + CCF7BC6F4B7334EFCB1BEC67 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + DEBFC7FD9205FF41F556DBF2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + F62E49ABA81F430B99525DED /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + FA1F5E9E308D75711360FAF6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -53,23 +72,52 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B9BC3BE22F97E1DD9B850D52 /* Pods_Runner.framework in Frameworks */, + 59185AECC1AC289A18E08B27 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FA8382EB2FA8D2065DE8E9F1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 936BDC305018E86F43152805 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 9511C1F4E928E9B5DC1D54EE /* Pods */ = { + 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( - 7404F988052D3E92A73EBD7E /* Pods-Runner.debug.xcconfig */, - F597851B8230B83CB87D4CF3 /* Pods-Runner.release.xcconfig */, + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 3EE7EBE41B6B5FCD51539099 /* Pods */ = { + isa = PBXGroup; + children = ( + CCF7BC6F4B7334EFCB1BEC67 /* Pods-Runner.debug.xcconfig */, + DEBFC7FD9205FF41F556DBF2 /* Pods-Runner.release.xcconfig */, + 08484B4D3782F96E7B6281AA /* Pods-Runner.profile.xcconfig */, + F62E49ABA81F430B99525DED /* Pods-RunnerTests.debug.xcconfig */, + 014F010A93253295F5620238 /* Pods-RunnerTests.release.xcconfig */, + 113E5C815831E8E83B1D24EE /* Pods-RunnerTests.profile.xcconfig */, ); name = Pods; path = Pods; sourceTree = ""; }; + 8E7C32B957AAD8E132B67CCE /* Frameworks */ = { + isa = PBXGroup; + children = ( + FA1F5E9E308D75711360FAF6 /* Pods_Runner.framework */, + 4B99FC96601B9D60C00E3472 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -87,8 +135,9 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - 9511C1F4E928E9B5DC1D54EE /* Pods */, - AA777AFD3B6B2AF061748624 /* Frameworks */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 3EE7EBE41B6B5FCD51539099 /* Pods */, + 8E7C32B957AAD8E132B67CCE /* Frameworks */, ); sourceTree = ""; }; @@ -96,6 +145,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -107,7 +157,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, @@ -116,29 +165,33 @@ path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - ); - name = "Supporting Files"; - sourceTree = ""; - }; - AA777AFD3B6B2AF061748624 /* Frameworks */ = { - isa = PBXGroup; - children = ( - B53E79FD7E80803BF54D70DB /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + F83437C9E1D805C7C1B2D2B3 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + FA8382EB2FA8D2065DE8E9F1 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 7A815B17627E798BC9FBBF3E /* [CP] Check Pods Manifest.lock */, + 91679AE7AACFDEE8A4C79959 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -161,18 +214,23 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; - ORGANIZATIONNAME = "The Chromium Authors"; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 0910; + LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -184,11 +242,19 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -219,7 +285,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 7A815B17627E798BC9FBBF3E /* [CP] Check Pods Manifest.lock */ = { + 91679AE7AACFDEE8A4C79959 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -256,9 +322,39 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + F83437C9E1D805C7C1B2D2B3 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -270,6 +366,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -290,10 +394,134 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.tundralabs.fluttertts.example.flutterTtsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F62E49ABA81F430B99525DED /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tundralabs.fluttertts.example.flutterTtsExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 014F010A93253295F5620238 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tundralabs.fluttertts.example.flutterTtsExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 113E5C815831E8E83B1D24EE /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tundralabs.fluttertts.example.flutterTtsExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -323,6 +551,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -337,7 +566,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -349,6 +578,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -378,6 +608,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -386,10 +617,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -401,24 +634,18 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.tundralabs.flutterTtsExample; + PRODUCT_BUNDLE_IDENTIFIER = com.tundralabs.fluttertts.example.flutterTtsExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -429,23 +656,17 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.tundralabs.flutterTtsExample; + PRODUCT_BUNDLE_IDENTIFIER = com.tundralabs.fluttertts.example.flutterTtsExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -453,11 +674,22 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -467,6 +699,7 @@ buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 41adb77b..e3773d42 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + + + + + @@ -61,11 +73,9 @@ ReferencedContainer = "container:Runner.xcodeproj"> - - Bool { GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 3d43d11e66f4de3da27ed045ca4fe38ad8b48094..dc9ada4725e9b0ddb1deab583e5b5102493aa332 100644 GIT binary patch literal 10932 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 P-1+_A+?hGkL;m(J7kc&K diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 28c6bf03016f6c994b70f38d1b7346e5831b531f..7353c41ecf9ca08017312dc233d9830079b50717 100644 GIT binary patch delta 279 zcmV+y0qFj;1g8R!8Gi!+006pI?LPnj0Blf9R7L;)|5U~J`u_j-{Qm)0oAmqtj@kOz z^8J|I`-|B6ht~R5kG+%I`zf~eztraM`u^bc{`dO)zUlmg)%x%C`E}6wSI77~z4s`y z^XT{f(eM4n?EUff`e@AgO~UxV*5*r_%Uhbj5N)LaQj!wdIe!-b004GLL_t&-)18pX z4udcZ1u-#g(~z+5JN*AY5?>Gw7hsN~k)CYt4dQDFxbs5*_&e@Hj)wtt(&JE<3Eq*D z;_gQLvqXoKv=I*gWqM9C(Tvu0>=?hTbOp9!6k6AF;>f6|S5%jGEE}TA9h)e`Yuiu8 d7)l?o1NFcJg%EAfM$P~L002ovPDHLkV1g^Dnv?(l delta 550 zcmV+>0@?ki0<;8>8Gi-<0051N9Sr~g00DDSM?wIu&K&6g00HhvL_t(I5v`QFOB_)Y z#?QI;j_a;jjf#Z$YJ7mH(xecJU?W)A`9CN~KrBV85C}GDQ=|;GDFPNjtWty!L{u=? zh>8yo%^GE+J9o~_IZFoiamQVQXP7%LzTbT3F@uf+9x&7cvVV%GdjTaC;zf>@mq<=3 z!c<%*UT)@yJ|0BK6~d4Jx-*KV`ZQ(@VyUPupum=XhInNG#Z_k-X|hK{B}~9IfiWx} zLD5QY6Vm)p0NrWymdkrHPN5Vgwd>5>4HI1=@PA+e^rq~CEj|n2X`??)0mUI*D{KBn zjv{V=y5X9|X@3grkpcXC6oou4ML~ezCc2EtnsQTB4tWNg?4bkf;hG7IMfhgNI(FV5 zGs4|*GyMTIY0$B=_*mso9+>eB z?J{?+FLkYu+4_Uk`r_>LHF~flZm0oBf#vr8%vJ>#p~!KNvqGG3)|f1T_)ydeh8$vDceZ>oNbH^|*hJ*t?Yc*1`WB&W>VYVEzu) zq#7;;VjO)t*nbgf(!`OXJBr45rP>>AQr$6c7slJWvbpNW@KTwna6d?PP>hvXCcp=4 zF;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f<+$JJpcdz delta 1274 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 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cde12118dda48d71e01fcb589a74d069c5d7cb5..4cd7b0099ca80c806f8fe495613e8d6c69460d76 100644 GIT binary patch delta 266 zcmV+l0rmcY2$}+r8Gi!+003c4mpuRg09{Z_R7L;)|5U~JDYo_jSDX9(|7FYh`2GLd z^Zv2r{H^2sT*&w!Y^SB+`<>qVZqE6)=lqo0`vF#&*75!I`TIh@_d&k*HoEtQyV-iD z%Xz2D9EQRbeYh5Nr~y=#0ZD;^+vz0$004MNL_t(2&&|%+4u6C&2tZM$Wf&dzefR%A z(^3-?6X>hnCz2Ba@RH&`m!pgy?n@#@AuLYB&}Q)FGY`?vcft0!vht0Z@M&ZeNCWXh75gzRTXR8EE3oN&6 Q00000NkvXXt^-0~g5kS`djJ3c delta 1014 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 Zxpx7IC@PekH?;r&002ovPDHLkV1i)CYaajr delta 1916 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 delta 1274 zcmV@pi1MCNO0zH7s z{8#}P0)7Ba8DqYf&QgSne>X__O83t$NZM4&R0{XJq|x}oAU?tcfC@|eNz$04T}34& z8DJf78R&>*Zz`k$q{`#gfGHnx7nlH^G{y`jfER)1<_fNi<9aM%_zrm1C`yPkKma(+ ztQ;y*CR2bbBYz>zG*SVsfpkGU(q>uHZf3iogk_%#9E|5SWeHrmAo>P;ejX7mwq#*} zW25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+X$F_KMdb6sRz!~7K zkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&IDi_4_D!s#MVXp|-XhH;H z#&@_;oApJVd}}5O@b=X_gJboD^-fM@6|#V@sA%X)Rlkd}3MLH0dGXGG&-HX|aD~|M zC)W#H7=H?AbtdaV#dGpubj_O^J-SlWpVNv-5(;wR%mvE9`Qaqo>03b&##eNNf=m#B z9@^lsd8tJ;BvI86kNV zc~0CY(7V{s+h%cWG|y=gt|q`z$l<(@qU=i?9q#uz`G?PgDMK!VMGidHZt*N+1L0ZI zFkH=mFtywc6rJ}C_?)=m)18V!ZQ`*-j(D`gCFK|nt#{bk*%%zuQ7o7kvJgA^=(^7b zzkm5GZ;jxRn{Wup8IOUx8D4uh&(=Ox-7$a;U><*5L^!% zxRlw)vAbh;sdlR||&e}8_8%)c2Fwy=F& zH|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}Jb#viX>Oi;kBKp1x_fc0#UIbIeSJ^EkWFox zijdim{ojmn@#7EC*aY;fC0W*WN+DmQtE06pNK3SfZ^#@2K`6RgEuU_KwJTQ>E?Yar zc_9e#I$F8%>kuy-JI6ocSsYvQGbsxUCx04(w1z-pMRz9`kH5smmF@WHEG?dcYkv){ zV?kn3XB$_3zr*h1Uow)(<5)w5;3Wh1jHI)`ZlXp&!yEV{Y_~@;?CLwq;4eeaGOe6( zEsSSbwSGD0-`dUUl014$1_O8Gi!+006nq0-pc?0H{z*R7L;)|5U~JDYo_jSDXF*|5nEMy6F5^ z$M}8I`uzU?*Yf=uXr;5|{0m;6_Wb|A>ik^D_|)+I$?g3CSDK^3+eX0mD!2CP`2NN0 z{dLg!a?km&%iyTt`yiax0acdp`~T(l{$a`ZF1YpsRg(cvjDG_-U$Er-fz#Bw>2W$eUI#iU z)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G!hkE!s;%oku3;IwG3U^2k zw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn_j%}l|2+O?a>_7qq7W zmx(qtA2nV^tZlLpy_#$U%ZNx5;$`0L&dZ!@e7rFXPGAOup%q`|03hpdtXsPP0000< KMNUMnLSTZ1N;Pr- delta 1891 zcmV-p2b}oI1m_Nr8Gi-<0052=@~r>>2QEoOK~#9!?VW3E6jc<*XLh$yKNt;)Mial3 z7z%<>zxaV5DhMs*(b6YIW1=KP6Jj(m21QYbiJ}su&;o5EN=$%gptMj6p|(7#AOTUJ zlt8fsX(iGq?ZQ50=XmbU+~w|cmz~|6$KBbz$-g^IcV>Hk`+q<8%-p?uMi3G-0B~!5 ze-yPCwFPw?HGmpMc~K)7BCq;C528+>zC*o^8h^XKC)IFgkv#xzm!ewK7j|kRa9dFo zC>MoDSR@P2#cWSU{i1oH5K2-Xb3jRz>|h7VOh0K` zhq^--L3H}A0r)nr z;Tr|-kPjB1s=ItpnS`oT%|U=a4oK-ZFIE^YBLH{u2#~@%%D^K)$`9*Tg(~9M-B+Zj z;~H?4LVsEt0eFtN4&>H(DZ@KpI6RhBKLL21CxC`J&m4Gc^9wwMZU#7SR1+KtuhSZM z+yLY}Vekzw6T_ApfEkuB_yU;e&a)L@rX~z70A_N+upOXN!qygmPDmKG0d%7CECcAI zgkd>ArzH$a0XjKsO$X@IgkcH5Y;m3`0G*yNOn(KK4GF_EfL4aB5i1j9o&Z{vFk~k> z&?@K2jQcJO%W!cddG(_DyfSoO55bUMHtbDF8DPkwF^~Ql#Eq4w15k{h%ML5Ar&pzi zl-D7v8kQXQ!&RRgKCW#5DZB$$6?mjWm50rRw*ukK>P-GkA|k69h{NARc>e}uLx+U4 z0DqE>7pa}9Fez+Vc-3jb`%i^uulglFoMzAVR|2%rf= zf#;74FXF^Ku_4+G&-4$KVy%YP>%2rxu2VG_cdm?XRjEhF&wPXJ># z_Q2+jGs=l~Fyx#MmGn+PZ0`@kBfGp|fO;Vov<$;z`(+sSZ7;Y=zXaF(8rb@CuQDV^ zq3i(2LfqO%AS!Ss>V%j7%>{6mtbYQrtQK5V4InPq0NZSaXv+f2U=&2}Z6OvkBfNHi z{LSaVJ!d5dC2K*ft_L^DRk;boQhOoVw!~Kt#0b2vd%!(&DF|~u1F@nG#LA5zR&7Fv z4GKgXooMSKb1g)6Obo-rgpuEP20T;W0Aa>55KC4gtQrKkAq-Hgs@FigV1GG8+rQ=z z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRet3L_uNyQ*c zE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=kyx=~RKa4{iT zm{_>_vSCm?$Ej=i6@=m%@PE9t1zZaoM}@2|h!#1K02~31S_I<0ZV=|K0}n!RRX6Ac zXmMf*5P-dLW}WPVsCKq)-x(0*txpZ2xrv3cxJ%l=7lpoNCyG< zK92ySAcmb-3m&}s@VwXv9(0#p<>B-5$bMxT;rk;OmENa6eM4D&LVo~01soUL39?R{ zyFLt3m|v?rCK7#KNu9E9Q4KV-pEUv^{rrClE&X&9I4-e7%pu_31#zGTOfC=ab%w20R*zBP+uT#l2{a~~~0wuG%6 zco*tVxK&e>%SJj*K!2tq*_h&ES5S9@TKb8WzpK;`&b9dNdxh4S)z%Q)o`aYWUh}9L z(`p!#WO5IxI|nf?yz{90R93Ed6@2qim*}Zjj$H&Esd`?JsFJUnDfiAgF_eYiWR3GC z>M9SHDylEWrA(%mfm~;u7OU9!Wz^!7Z%jZF zi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i0WYBP d*#0Ks^FNSabJA*5${_#%002ovPDHLkV1gB0Vle;! diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index a6d6b8609df07bf62e5100a53a01510388bd2b22..0ec303439225b78712f49115768196d8d76f6790 100644 GIT binary patch delta 850 zcmV-Y1Fih&6y64q8Gi!+000iU#^3+|0OwFlR7L;)|5U~J09TtSw)Xt~|5(QO`~Ck( z!T0|D|3<*~RmJ%E{r+;#`2ba!klFf7!uJMSo%Q?vP{jByxcAZE>;OrUCbaZYjJo^$ z{nGILmD~Da$@upC{`C6(Ey4dPw)Pyc^>5DkHoEo!QcuK-Jwl-l}t(fQKv z{dds$V#@dygS`PvhX6is7Z+@*x-d;$ zb=6f@U3Jw}_s+W3%*+b9H_vS)-R#9?zrXogeLVI2We2RFTTAL}&3C8PS~<5D&v@UI z+`s*$wqQ=yd$laNUY-|ovcS9~n_90tFUdl#qq0tEUXle|k{Op|DHpSrbxEeZ5~$>o%>OSe z^=41qvh3LlC2xXzu+-2eQoqs1^L>7ylB$bCP);(%(xYZL1 cY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f@rA97ytkO literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index a6d6b8609df07bf62e5100a53a01510388bd2b22..0ec303439225b78712f49115768196d8d76f6790 100644 GIT binary patch delta 850 zcmV-Y1Fih&6y64q8Gi!+000iU#^3+|0OwFlR7L;)|5U~J09TtSw)Xt~|5(QO`~Ck( z!T0|D|3<*~RmJ%E{r+;#`2ba!klFf7!uJMSo%Q?vP{jByxcAZE>;OrUCbaZYjJo^$ z{nGILmD~Da$@upC{`C6(Ey4dPw)Pyc^>5DkHoEo!QcuK-Jwl-l}t(fQKv z{dds$V#@dygS`PvhX6is7Z+@*x-d;$ zb=6f@U3Jw}_s+W3%*+b9H_vS)-R#9?zrXogeLVI2We2RFTTAL}&3C8PS~<5D&v@UI z+`s*$wqQ=yd$laNUY-|ovcS9~n_90tFUdl#qq0tEUXle|k{Op|DHpSrbxEeZ5~$>o%>OSe z^=41qvh3LlC2xXzu+-2eQoqs1^L>7ylB$bCP);(%(xYZL1 cY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f@rA97ytkO literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..e9f5fea27c705180eb716271f41b582e76dcbd90 100644 GIT binary patch delta 1668 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 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..84ac32ae7d989f82d5e46a60405adcc8279e8001 100644 GIT binary patch delta 749 zcmVg;Ps8|O$@u8^{Z_{KM!@$5TAfS6_e#O{MZfpz`2O`0$7~@NRr(1{THzH08y3x{{PYM{eL;T_A9^tcF_4Sxb`8l z_9V3RD6;a(-0A^Pjsi!1?)d#Ap4Tk3^CP0(07;VpJ7@tgQ}z4)*zx@&yZwC9`DV-b z0ZobH_5IB4{KxD3;p_6%|f=bdFhu+F!zMZ2UFj;GUKX7tI;hv3{q~!*pMj75WP_c}> z6)IWvg5_yyg<9Op()eD1hWC19M@?_9_MHec{Z8n3FMs~w_u?Av_yNBmRxVYrpi(M% zFMP21g+hmocQp3ay*Su=qM6He)*HaaTg$E^sym`(t%s3A)x!M+vfjXUBEpK6X9%iU zU!u9jj3(-$dM~sJ%Liy#?|+!6IY#MTau#O6vVj`yh_7%Ni!?!VS+MPTO(_fG+1<#p zqu;A#i+_(N%CmVnYvb>#nA{>Q%3E`Ds7<~jZMywn@h2t>G-LrYy7?Dj{aZqhQd6tzX%(Trn+ z)HNF}%-F{rr=m*0{=a;s#YDL00000NkvXXu0mjfaGjYE delta 1884 zcmV-i2c!7<1>g>l8Gi-<0076AQ7Zrd2Pa8HK~#9!?VNjT6h$1z_m0EFf5bmb1dTDK zp;kdKV1h(V(8Sc1M<37!RE>znAk{x4#zX@eOeE1j3~!+nB5IL z<xS}u?#DBMB>w^b($1Z)`9G?eP95EKi& z$eOy@K%h;ryrR3la%;>|o*>CgB(s>dDcNOXg}CK9SPmD?Uu$P4(=PGA0ShFasNfcIHTL?9WjB9#(2xSLC z`0%$#9DW9F;B4mbU{BlaYx!SjF!QSeF~(msQRxwboh5B_O$BWOQja)GboJz$&!?mgB&3$ytsA zvns&b3Cl5Hx#%p%faR*Q906u&fbXy$maV`n?S>A)vJIH!F-vxCrY+rq5_JA(GcOgu7(Ky4X3ATR9z8*%k&<5qYeV&4Y`~}XYmK(j{)!g8d2UgHXIINM!Rvn zKtEq~Foe0s!U{kux~F6Y7Sp+2f|*Cc${S{@oh8D0=XhB8Ec-w9CflfL+te4ium2cU zoPTCj_m<3d#gjK=<*8R`HP^C$lOPM5d~UhKhRRmvv{LI za^|oavk1$QiEApSrP@~Jjbg`<*dW4TO@DPEEX$Tg$xh?Y>Qd}y@kaH~IT8!lLpS^J zR7(&wZSI6+>Eb)tX>9Z?GX#q$u z4I>7e#b7ojyJ1grOh!^}s8S#ubi^Jkd1?UK)3mp6rI^_zxRY zrx6_QmhoWoDR`fp4R7gu6@OBFGu7IDVR6~nJsB{^f5jHn<{WJ&&f^X?3f8TIk3#U& zu1*Q-e@;snJxNx8-PBnpI|uFTKN!+Lp;fPfZ+eqqU^Y1|#DJY~126?zOx-+d>%4*? z&o`TbrXSNXZW^!P0t2>@$6&aiBtUDh2wLXLD9&a(1J=k_FK|iGbAQ@x4Qmx}Ms+*; zze&q6bH(=wYuXHfz0H6<05!LkE4rl~v^!bj=^9d+vI5fN<;GP>*Pas=q2l9RxDkk` zPRk&EQI+t_0$Y%nKE)Ma)W?jaA@4Z{h zTk*7;;#lG?hvTN_On=Jaxp%bdE;mDq(q#dgdYF|-?wrMeI4h`$idZ6^VyXZVlaCd0 z;i)OYR3npf@9>00Gqn##Zb4HRurgaWFCzL9u6@J@sse>Z1XznxWvSy%Td32I3!#YN zXt9v0)RQtDDZRd?#WY?~KF7A0UcR{jt9 W+;fr}hV%pg0000&=UXv0SHh`R7L;)|5U~JDYo_jSDRDC`1<|-SjPDL z{{Q{{{{H{}09Kk-#rR9Y_viNgVafPO!S|ls`uzR=MZfp^{QU=8od8La1X`Tr_Wmff z_5e$ivgQ1@=KMy$_g9a+`TPAle6cOJ_Fc#L7qIpvwDkd1mw$fK`6IOUD75rX!}mad zv(fMTE4=(Nx%L54lL1hVF1YpqNrC`FddBPg#_Ietx%Lrkq5wX00X1L{S%Cm9QY*av z#_Rh5PKy9KYTWbvz3BX9%J>0Hi1+#X{rLA{m%$Kamk?i!03AC38#Yrxs)5QTeTVRiEmz~MKK1WAjCw(c-JK6eox;2O)?`?TG`AHia671e^vgmp!llK zp|=5sVHk#C7=~epA~VAf-~%aPC=%Qw01h8mnSZ|p?tc*y?iZ$PR7_ceEIapF3KB14K0Pog?7wtd+^xgUCa_GVmlD z<^nU>AU_Yn-JU?NFdu|wf^bTCNf-wSBYVZltDdvGBln-YrbeGvJ!|s{#`gjN@yAMb zM6cjFz0eFECCsc|_8hTa3*9-JQGehksdoVP^K4m?&wpA~+|b%{EP5D-+7h)6CE; z*{>BP=GRR3Ea}xyV*bqry{l^J=0#DaC4ej;1qs8_by?H6Tr@7hl>UKNZt)^B&yl;)&oqzLg zcfZxpE?3k%_iTOVywh%`XVN-E#COl+($9{v(pqSQcrz=)>G!!3HeNxbXGM@})1|9g zG4*@(OBaMvY0P0_TfMFPh fVHk#CZX3S=^^2mI>Ux-D00000NkvXXu0mjfzK(<8 literal 3294 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 cX)(b1KSk$MOV07*qoM6N<$f&{Qds= z{r_0T`1}6fwc-8!#-TGX}_?g)CZq4{k!uZ_g@DrQdoW0kI zu+W69&uN^)W`CK&06mMNcYMVF00dG=L_t(|+U?wHQxh>12H+Dm+1+fh+IF>G0SjJM zkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJTkdTm&kdTm&kdTm&kdP`e zsgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>VI$fQI%^ugM`#6By?GeadWcu z0gy9!D`m!H>Bd!JW(@avE8`|5XX(0PN}!8K>`dkavs;rHL+wy96QGNT=S@#7%xtlm zIW!++@*2zm-Py#Zr`DzqsLm!b{iskFNULSqE9A>SqHem>o31A%XL>S_5?=;V_i_y+ z(xxXhnt#r-l1Y8_*h`r?8Tr|)(RAiO)4jQR`13X0mx07C&p@KBP_2s``KEhv^|*8c z$$_T(v6^1Ig=#R}sE{vjA?ErGDZGUsyoJuWdJMc7Nb1^KF)-u<7q zPy$=;)0>vuWuK2hQhswLf!9yg`88u&eBbR8uhod?Nw09AXH}-#qOLLxeT2%C;R)QQ$Za#qp~cM&YVmS4i-*Fpd!cC zBXc?(4wcg>sHmXGd^VdE<5QX{Kyz$;$sCPl(_*-P2Iw?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF34$0Z;QO!J zOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUCUoZo%k(yku QW&i*H07*qoM6N<$g47z!?*IS* literal 3612 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 iW8 CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Flutter Tts Example CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,21 +17,17 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion - 1 + $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main - UIRequiredDeviceCapabilities - - arm64 - UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -43,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h index 7335fdf9..308a2a56 100644 --- a/example/ios/Runner/Runner-Bridging-Header.h +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -1 +1 @@ -#import "GeneratedPluginRegistrant.h" \ No newline at end of file +#import "GeneratedPluginRegistrant.h" diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/example/lib/main.dart b/example/lib/main.dart index ae748393..922b55e8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_print + import 'dart:async'; import 'dart:io' show Platform; @@ -5,19 +7,41 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_tts/flutter_tts.dart'; -void main() => runApp(MyApp()); +extension on Voice { + String get displayName { + final elements = [name, locale]; + if (gender != null) { + elements.add(gender!); + } + if (quality != null) { + elements.add(quality!); + } + return elements.join(' - '); + } +} + +void main() { + runZonedGuarded(() => runApp(MyApp()), (error, stackTrace) { + print("Error: $error"); + print("Stack Trace: $stackTrace"); + }); +} class MyApp extends StatefulWidget { + const MyApp({super.key}); + @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } enum TtsState { playing, stopped, paused, continued } class _MyAppState extends State { - late FlutterTts flutterTts; + late FlutterTtsPlatform flutterTts; String? language; String? engine; + Voice? voice; + final voices = []; double volume = 0.5; double pitch = 1.0; double rate = 0.5; @@ -25,6 +49,7 @@ class _MyAppState extends State { String? _newVoiceText; int? _inputLength; + final _editingController = TextEditingController(); TtsState ttsState = TtsState.stopped; @@ -45,7 +70,7 @@ class _MyAppState extends State { } dynamic initTts() { - flutterTts = FlutterTts(); + flutterTts = FlutterTts.platform; _setAwaitOptions(); @@ -54,64 +79,112 @@ class _MyAppState extends State { _getDefaultVoice(); } - flutterTts.setStartHandler(() { + flutterTts.onSpeakStart = () { setState(() { print("Playing"); ttsState = TtsState.playing; }); - }); + }; - flutterTts.setCompletionHandler(() { + flutterTts.onSpeakComplete = () { setState(() { print("Complete"); ttsState = TtsState.stopped; }); - }); + }; - flutterTts.setCancelHandler(() { + flutterTts.onSpeakCancel = () { setState(() { print("Cancel"); ttsState = TtsState.stopped; }); - }); + }; - flutterTts.setPauseHandler(() { + flutterTts.onSpeakPause = () { setState(() { print("Paused"); ttsState = TtsState.paused; }); - }); + }; - flutterTts.setContinueHandler(() { + flutterTts.onSpeakResume = () { setState(() { print("Continued"); ttsState = TtsState.continued; }); - }); + }; - flutterTts.setErrorHandler((msg) { + flutterTts.onSpeakError = (msg) { + print("error: $msg"); setState(() { - print("error: $msg"); ttsState = TtsState.stopped; }); + }; + + flutterTts.getVoices().then((value) { + value.when( + (newVoices) => setState(() { + voices.clear(); + voices.addAll(newVoices); + }), + (e) => print("Error: $e"), + ); }); } - Future _getLanguages() async => await flutterTts.getLanguages; + Future> _getLanguages() async { + final languages = await flutterTts.getLanguages(); + switch (languages) { + case SuccessDart(): + return languages.success; + case FailureDart(): + print("Error: ${languages.error}"); + return []; + } + } + + Future> _getEngines() async { + if (flutterTts case final FlutterTtsAndroid tts) { + final engines = await tts.getEngines(); + switch (engines) { + case SuccessDart(success: var newEngines): + return newEngines; + case FailureDart(error: var e): + print("Error: $e"); + return []; + } + } - Future _getEngines() async => await flutterTts.getEngines; + return []; + } Future _getDefaultEngine() async { - var engine = await flutterTts.getDefaultEngine; - if (engine != null) { - print(engine); + if (flutterTts case final FlutterTtsAndroid tts) { + final defaultEngine = await tts.getDefaultEngine(); + switch (defaultEngine) { + case SuccessDart(success: var newEngine): + engine = newEngine; + break; + case FailureDart(error: var e): + print("Error: $e"); + engine = null; + break; + } } } Future _getDefaultVoice() async { - var voice = await flutterTts.getDefaultVoice; - if (voice != null) { - print(voice); + if (flutterTts case final FlutterTtsAndroid tts) { + final defVoice = await tts.getDefaultVoice(); + switch (defVoice) { + case SuccessDart(): + voice = defVoice.success; + break; + case FailureDart(): + print("Error: ${defVoice.error}"); + voice = null; + break; + } } } @@ -133,12 +206,30 @@ class _MyAppState extends State { Future _stop() async { var result = await flutterTts.stop(); - if (result == 1) setState(() => ttsState = TtsState.stopped); + switch (result) { + case SuccessDart(): + if (result.success.success) { + setState(() => ttsState = TtsState.stopped); + } + break; + case FailureDart(): + print("Error: ${result.error}"); + break; + } } Future _pause() async { var result = await flutterTts.pause(); - if (result == 1) setState(() => ttsState = TtsState.paused); + switch (result) { + case SuccessDart(): + if (result.success.success) { + setState(() => ttsState = TtsState.paused); + } + break; + case FailureDart(): + print("Error: ${result.error}"); + break; + } } @override @@ -148,42 +239,49 @@ class _MyAppState extends State { } List> getEnginesDropDownMenuItems( - List engines) { + List engines, + ) { var items = >[]; for (dynamic type in engines) { - items.add(DropdownMenuItem( - value: type as String?, child: Text((type as String)))); + items.add( + DropdownMenuItem(value: type as String?, child: Text((type as String))), + ); } return items; } void changedEnginesDropDownItem(String? selectedEngine) async { - await flutterTts.setEngine(selectedEngine!); - language = null; - setState(() { - engine = selectedEngine; - }); + if (selectedEngine != null && selectedEngine.isNotEmpty) { + if (flutterTts case final FlutterTtsAndroid tts) { + await tts.setEngine(selectedEngine); + setState(() { + engine = selectedEngine; + language = null; + voice = null; + voices.clear(); + }); + } + } } List> getLanguageDropDownMenuItems( - List languages) { + List languages, + ) { var items = >[]; for (dynamic type in languages) { - items.add(DropdownMenuItem( - value: type as String?, child: Text((type as String)))); + items.add( + DropdownMenuItem(value: type as String?, child: Text((type as String))), + ); } return items; } - void changedLanguageDropDownItem(String? selectedType) { + void changedLanguageDropDownItem(String? selectedType) async { + var newIsCurrentLanguageInstalled = false; setState(() { language = selectedType; - flutterTts.setLanguage(language!); - if (isAndroid) { - flutterTts - .isLanguageInstalled(language!) - .then((value) => isCurrentLanguageInstalled = (value as bool)); - } + voice = null; + isCurrentLanguageInstalled = newIsCurrentLanguageInstalled; }); } @@ -196,10 +294,11 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { return MaterialApp( + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + themeMode: ThemeMode.system, home: Scaffold( - appBar: AppBar( - title: Text('Flutter TTS'), - ), + appBar: AppBar(title: Text('Flutter TTS')), body: SingleChildScrollView( scrollDirection: Axis.vertical, child: Column( @@ -208,6 +307,12 @@ class _MyAppState extends State { _btnSection(), _engineSection(), _futureBuilder(), + Row( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [_buildGetVoiceBtn(), _buildAddTextToSpeak()], + ), + if (voices.isNotEmpty) _buildSelectVoice(), _buildSliders(), if (isAndroid) _getMaxSpeechInputLengthSection(), ], @@ -217,45 +322,122 @@ class _MyAppState extends State { ); } + TextButton _buildAddTextToSpeak() { + return TextButton( + onPressed: () { + _newVoiceText = """ +The quick brown fox jumps over the lazy dog. +混沌未分天地乱,茫茫渺渺无人见。 自从盘古破鸿蒙,开辟从兹清浊辨。 覆载群生仰至仁,发明万物皆为善。 欲知造化会元功,须看《西游释厄传》。 +The quick brown fox jumps over the lazy dog. +我说"1<2" & 3>0,对吧? +Whether the weather be fine or whether the weather be not. +Whether the weather be cold or whether the weather be hot. +We'll weather the weather whether we like it or not. +季姬寂,集鸡,鸡即棘鸡。棘鸡饥叽,季姬及箕稷济鸡。鸡既济,跻姬笈,季姬忌,急咭鸡,鸡急,继圾几,季姬急,即籍箕击鸡,箕疾击几伎,伎即齑,鸡叽集几基,季姬急极屐击鸡,鸡既殛,季姬激,即记《季姬击鸡记》。 +なまむぎ なまごめ なまたまご +간장공장 공장장은 강공장장이고된장공장 공장장은 공공장장이다 +Cinq chiens chassent six chats. +На дворе-трава, на траве-дрова. Не руби дрова на траве-двора. +"""; + _editingController.text = _newVoiceText!; + }, + child: Text("Add Default Text"), + ); + } + + TextButton _buildGetVoiceBtn() { + return TextButton( + onPressed: () async { + var result = await flutterTts.getVoices(); + switch (result) { + case SuccessDart(): + setState(() { + voices.clear(); + voices.addAll(result.success); + }); + break; + case FailureDart(): + print("Error: ${result.error}"); + break; + } + }, + child: Text("Get Voices"), + ); + } + + DropdownButton _buildSelectVoice() { + final selectedLang = language?.split('-').first; + var voiceToShow = voices.where( + (element) => + selectedLang == null || element.locale.startsWith(selectedLang), + ); + + if (voiceToShow.isEmpty) { + voiceToShow = voices; + } + + return DropdownButton( + value: voice, + items: [ + for (final ii in voiceToShow) + DropdownMenuItem(value: ii, child: Text(ii.displayName)), + ], + onChanged: (value) { + setState(() { + voice = value; + }); + + if (value != null) { + flutterTts.setVoice(value); + } + }, + ); + } + Widget _engineSection() { if (isAndroid) { return FutureBuilder( - future: _getEngines(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return _enginesDropDownSection(snapshot.data as List); - } else if (snapshot.hasError) { - return Text('Error loading engines...'); - } else { - return Text('Loading engines...'); - } - }); + future: _getEngines(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return _enginesDropDownSection(snapshot.data as List); + } else if (snapshot.hasError) { + return Text('Error loading engines...'); + } else { + return Text('Loading engines...'); + } + }, + ); } else { - return Container(width: 0, height: 0); + return SizedBox(width: 0, height: 0); } } Widget _futureBuilder() => FutureBuilder( - future: _getLanguages(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return _languageDropDownSection(snapshot.data as List); - } else if (snapshot.hasError) { - return Text('Error loading languages...'); - } else - return Text('Loading Languages...'); - }); + future: _getLanguages(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return _languageDropDownSection(snapshot.data as List); + } else if (snapshot.hasError) { + return Text('Error loading languages...\n${snapshot.error}'); + } else { + return Text('Loading Languages...'); + } + }, + ); Widget _inputSection() => Container( - alignment: Alignment.topCenter, - padding: EdgeInsets.only(top: 25.0, left: 25.0, right: 25.0), - child: TextField( - maxLines: 11, - minLines: 6, - onChanged: (String value) { - _onChange(value); - }, - )); + alignment: Alignment.topCenter, + padding: EdgeInsets.only(top: 25.0, left: 25.0, right: 25.0), + child: TextField( + controller: _editingController, + maxLines: 11, + minLines: 6, + onChanged: (String value) { + _onChange(value); + }, + ), + ); Widget _btnSection() { return Container( @@ -263,29 +445,46 @@ class _MyAppState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _buildButtonColumn(Colors.green, Colors.greenAccent, Icons.play_arrow, - 'PLAY', _speak), _buildButtonColumn( - Colors.red, Colors.redAccent, Icons.stop, 'STOP', _stop), + Colors.green, + Colors.greenAccent, + Icons.play_arrow, + 'PLAY', + _speak, + ), + _buildButtonColumn( + Colors.red, + Colors.redAccent, + Icons.stop, + 'STOP', + _stop, + ), _buildButtonColumn( - Colors.blue, Colors.blueAccent, Icons.pause, 'PAUSE', _pause), + Colors.blue, + Colors.blueAccent, + Icons.pause, + 'PAUSE', + _pause, + ), ], ), ); } Widget _enginesDropDownSection(List engines) => Container( - padding: EdgeInsets.only(top: 50.0), - child: DropdownButton( - value: engine, - items: getEnginesDropDownMenuItems(engines), - onChanged: changedEnginesDropDownItem, - ), - ); + padding: EdgeInsets.only(top: 50.0), + child: DropdownButton( + value: engine, + items: getEnginesDropDownMenuItems(engines), + onChanged: changedEnginesDropDownItem, + ), + ); Widget _languageDropDownSection(List languages) => Container( - padding: EdgeInsets.only(top: 10.0), - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + padding: EdgeInsets.only(top: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ DropdownButton( value: language, items: getLanguageDropDownMenuItems(languages), @@ -295,27 +494,40 @@ class _MyAppState extends State { visible: isAndroid, child: Text("Is installed: $isCurrentLanguageInstalled"), ), - ])); - - Column _buildButtonColumn(Color color, Color splashColor, IconData icon, - String label, Function func) { + ], + ), + ); + + Column _buildButtonColumn( + Color color, + Color splashColor, + IconData icon, + String label, + Function func, + ) { return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(icon), + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(icon), + color: color, + splashColor: splashColor, + onPressed: () => func(), + ), + Container( + margin: const EdgeInsets.only(top: 8.0), + child: Text( + label, + style: TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.w400, color: color, - splashColor: splashColor, - onPressed: () => func()), - Container( - margin: const EdgeInsets.only(top: 8.0), - child: Text(label, - style: TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.w400, - color: color))) - ]); + ), + ), + ), + ], + ); } Widget _getMaxSpeechInputLengthSection() { @@ -324,10 +536,7 @@ class _MyAppState extends State { children: [ ElevatedButton( child: Text('Get max speech input length'), - onPressed: () async { - _inputLength = await flutterTts.getMaxSpeechInputLength; - setState(() {}); - }, + onPressed: () async {}, ), Text("$_inputLength characters"), ], @@ -335,28 +544,33 @@ class _MyAppState extends State { } Widget _buildSliders() { - return Column( - children: [_volume(), _pitch(), _rate()], - ); + return Column(children: [_volume(), _pitch(), _rate()]); } Widget _volume() { return Slider( - value: volume, - onChanged: (newVolume) { - setState(() => volume = newVolume); - }, - min: 0.0, - max: 1.0, - divisions: 10, - label: "Volume: ${volume.toStringAsFixed(1)}"); + value: volume, + onChanged: (newVolume) { + setState(() { + volume = newVolume; + flutterTts.setVolume(volume); + }); + }, + min: 0.0, + max: 1.0, + divisions: 10, + label: "Volume: ${volume.toStringAsFixed(1)}", + ); } Widget _pitch() { return Slider( value: pitch, onChanged: (newPitch) { - setState(() => pitch = newPitch); + setState(() { + pitch = newPitch; + flutterTts.setPitch(pitch); + }); }, min: 0.5, max: 2.0, @@ -370,11 +584,14 @@ class _MyAppState extends State { return Slider( value: rate, onChanged: (newRate) { - setState(() => rate = newRate); + setState(() { + rate = newRate; + flutterTts.setSpeechRate(rate); + }); }, min: 0.0, - max: 1.0, - divisions: 10, + max: 2.0, + divisions: 20, label: "Rate: ${rate.toStringAsFixed(1)}", activeColor: Colors.green, ); diff --git a/example/macos/.gitignore b/example/macos/.gitignore index f73015c4..746adbb6 100644 --- a/example/macos/.gitignore +++ b/example/macos/.gitignore @@ -1,7 +1,7 @@ # Flutter-related **/Flutter/ephemeral/ **/Pods/ -**/Flutter/GeneratedPluginRegistrant.swift # Xcode-related +**/dgph **/xcuserdata/ diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig index 785633d3..4b81f9b2 100644 --- a/example/macos/Flutter/Flutter-Debug.xcconfig +++ b/example/macos/Flutter/Flutter-Debug.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig index 5fba960c..5caa9d15 100644 --- a/example/macos/Flutter/Flutter-Release.xcconfig +++ b/example/macos/Flutter/Flutter-Release.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..1e6ac53b --- /dev/null +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import flutter_tts + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) +} diff --git a/example/macos/Podfile b/example/macos/Podfile index 9ec46f8c..ff5ddb3b 100644 --- a/example/macos/Podfile +++ b/example/macos/Podfile @@ -28,9 +28,11 @@ flutter_macos_podfile_setup target 'Runner' do use_frameworks! - use_modular_headers! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end end post_install do |installer| diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 7529558d..7a842fc2 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -21,15 +21,24 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - AF47F6675ACF6F652B5F619C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59757445EA1BFBFD80865428 /* Pods_Runner.framework */; }; + 81814821D98A230A3082EB4F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0DD57EF221AB951F22FF40F3 /* Pods_Runner.framework */; }; + A164236EF8D8116C89137765 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A9E7F1F99AEEE2115716D78 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; @@ -53,10 +62,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 1D4577081AF3B0ACCB9634B9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 0DD57EF221AB951F22FF40F3 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1A9E7F1F99AEEE2115716D78 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* flutter_tts_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flutter_tts_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -68,25 +80,44 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 59757445EA1BFBFD80865428 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7A444ED245F431EA6C18BDDF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 50544B26B78743C9E5BA9730 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 68D1BAE90FC8AACBF92FAE46 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 6F589161388A001191BBEB3E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 73479AF64B450DC9B8338EA3 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 83F1C99C4E6DECE1A2A77CD0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - C937088BEE8A4C6EFDA6C14D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + AAF8BEE663C9E43E751A266C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A164236EF8D8116C89137765 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - AF47F6675ACF6F652B5F619C /* Pods_Runner.framework in Frameworks */, + 81814821D98A230A3082EB4F /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( @@ -103,16 +134,18 @@ children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, - 5F9F48FBDE9334256A3D8D1C /* Pods */, - 653A588D5B8D527EE4357BF9 /* Frameworks */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + FCDE38BA0FB84A2FBC55BACD /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* example.app */, + 33CC10ED2044A3C60003C045 /* flutter_tts_example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -152,38 +185,62 @@ path = Runner; sourceTree = ""; }; - 5F9F48FBDE9334256A3D8D1C /* Pods */ = { + D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - C937088BEE8A4C6EFDA6C14D /* Pods-Runner.debug.xcconfig */, - 7A444ED245F431EA6C18BDDF /* Pods-Runner.release.xcconfig */, - 1D4577081AF3B0ACCB9634B9 /* Pods-Runner.profile.xcconfig */, + 0DD57EF221AB951F22FF40F3 /* Pods_Runner.framework */, + 1A9E7F1F99AEEE2115716D78 /* Pods_RunnerTests.framework */, ); - path = Pods; + name = Frameworks; sourceTree = ""; }; - 653A588D5B8D527EE4357BF9 /* Frameworks */ = { + FCDE38BA0FB84A2FBC55BACD /* Pods */ = { isa = PBXGroup; children = ( - 59757445EA1BFBFD80865428 /* Pods_Runner.framework */, - ); - name = Frameworks; + 6F589161388A001191BBEB3E /* Pods-Runner.debug.xcconfig */, + 83F1C99C4E6DECE1A2A77CD0 /* Pods-Runner.release.xcconfig */, + 73479AF64B450DC9B8338EA3 /* Pods-Runner.profile.xcconfig */, + AAF8BEE663C9E43E751A266C /* Pods-RunnerTests.debug.xcconfig */, + 50544B26B78743C9E5BA9730 /* Pods-RunnerTests.release.xcconfig */, + 68D1BAE90FC8AACBF92FAE46 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 9231E46C7194B080EB3F8C1D /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 33CC10EC2044A3C60003C045 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 55F474F11A569479CD85540F /* [CP] Check Pods Manifest.lock */, + EEFF4A371A563C47E79A4525 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 9A8FDD7662D2D721983EFE47 /* [CP] Embed Pods Frameworks */, + 43771EAC433B4215C3B41E58 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -192,7 +249,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productReference = 33CC10ED2044A3C60003C045 /* flutter_tts_example.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -201,10 +258,15 @@ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1150; - ORGANIZATIONNAME = "The Flutter Authors"; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; @@ -222,7 +284,7 @@ }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 8.0"; + compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -235,12 +297,20 @@ projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -255,6 +325,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -288,9 +359,26 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh\ntouch Flutter/ephemeral/tripwire\n"; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 43771EAC433B4215C3B41E58 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - 55F474F11A569479CD85540F /* [CP] Check Pods Manifest.lock */ = { + 9231E46C7194B080EB3F8C1D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -305,34 +393,46 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 9A8FDD7662D2D721983EFE47 /* [CP] Embed Pods Frameworks */ = { + EEFF4A371A563C47E79A4525 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/flutter_tts/flutter_tts.framework", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_tts.framework", + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 33CC10E92044A3C60003C045 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -346,6 +446,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; @@ -366,11 +471,57 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AAF8BEE663C9E43E751A266C /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tundralabs.fluttertts.example.flutterTtsExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_tts_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_tts_example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 50544B26B78743C9E5BA9730 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tundralabs.fluttertts.example.flutterTtsExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_tts_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_tts_example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 68D1BAE90FC8AACBF92FAE46 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tundralabs.fluttertts.example.flutterTtsExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_tts_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_tts_example"; + }; + name = Profile; + }; 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -394,9 +545,11 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -419,13 +572,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -449,6 +597,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -472,9 +621,11 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -502,6 +653,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -525,9 +677,11 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -550,13 +704,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -575,13 +724,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -611,6 +755,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 6111bd0a..a1c30175 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ @@ -31,19 +31,20 @@ + skipped = "NO" + parallelizable = "YES"> @@ -58,20 +59,21 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift index d53ef643..b3c17614 100644 --- a/example/macos/Runner/AppDelegate.swift +++ b/example/macos/Runner/AppDelegate.swift @@ -1,9 +1,13 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 3c4935a7ca84f0976aca34b7f2895d65fb94d1ea..82b6f9d9a33e198f5747104729e1fcef999772a5 100644 GIT binary patch literal 102994 zcmeEugo5nb1G~3xi~y`}h6XHx5j$(L*3|5S2UfkG$|UCNI>}4f?MfqZ+HW-sRW5RKHEm z^unW*Xx{AH_X3Xdvb%C(Bh6POqg==@d9j=5*}oEny_IS;M3==J`P0R!eD6s~N<36C z*%-OGYqd0AdWClO!Z!}Y1@@RkfeiQ$Ib_ z&fk%T;K9h`{`cX3Hu#?({4WgtmkR!u3ICS~|NqH^fdNz>51-9)OF{|bRLy*RBv#&1 z3Oi_gk=Y5;>`KbHf~w!`u}!&O%ou*Jzf|Sf?J&*f*K8cftMOKswn6|nb1*|!;qSrlw= zr-@X;zGRKs&T$y8ENnFU@_Z~puu(4~Ir)>rbYp{zxcF*!EPS6{(&J}qYpWeqrPWW< zfaApz%<-=KqxrqLLFeV3w0-a0rEaz9&vv^0ZfU%gt9xJ8?=byvNSb%3hF^X_n7`(fMA;C&~( zM$cQvQ|g9X)1AqFvbp^B{JEX$o;4iPi?+v(!wYrN{L}l%e#5y{j+1NMiT-8=2VrCP zmFX9=IZyAYA5c2!QO96Ea-6;v6*$#ZKM-`%JCJtrA3d~6h{u+5oaTaGE)q2b+HvdZ zvHlY&9H&QJ5|uG@wDt1h99>DdHy5hsx)bN`&G@BpxAHh$17yWDyw_jQhhjSqZ=e_k z_|r3=_|`q~uA47y;hv=6-o6z~)gO}ZM9AqDJsR$KCHKH;QIULT)(d;oKTSPDJ}Jx~G#w-(^r<{GcBC*~4bNjfwHBumoPbU}M)O za6Hc2ik)2w37Yyg!YiMq<>Aov?F2l}wTe+>h^YXcK=aesey^i)QC_p~S zp%-lS5%)I29WfywP(r4@UZ@XmTkqo51zV$|U|~Lcap##PBJ}w2b4*kt7x6`agP34^ z5fzu_8rrH+)2u*CPcr6I`gL^cI`R2WUkLDE5*PX)eJU@H3HL$~o_y8oMRoQ0WF9w| z6^HZDKKRDG2g;r8Z4bn+iJNFV(CG;K-j2>aj229gl_C6n12Jh$$h!}KVhn>*f>KcH z;^8s3t(ccVZ5<{>ZJK@Z`hn_jL{bP8Yn(XkwfRm?GlEHy=T($8Z1Mq**IM`zxN9>-yXTjfB18m_$E^JEaYn>pj`V?n#Xu;Z}#$- zw0Vw;T*&9TK$tKI7nBk9NkHzL++dZ^;<|F6KBYh2+XP-b;u`Wy{~79b%IBZa3h*3^ zF&BKfQ@Ej{7ku_#W#mNJEYYp=)bRMUXhLy2+SPMfGn;oBsiG_6KNL8{p1DjuB$UZB zA)a~BkL)7?LJXlCc}bB~j9>4s7tlnRHC5|wnycQPF_jLl!Avs2C3^lWOlHH&v`nGd zf&U!fn!JcZWha`Pl-B3XEe;(ks^`=Z5R zWyQR0u|do2`K3ec=YmWGt5Bwbu|uBW;6D8}J3{Uep7_>L6b4%(d=V4m#(I=gkn4HT zYni3cnn>@F@Wr<hFAY3Y~dW+3bte;70;G?kTn4Aw5nZ^s5|47 z4$rCHCW%9qa4)4vE%^QPMGf!ET!^LutY$G zqdT(ub5T5b+wi+OrV}z3msoy<4)`IPdHsHJggmog0K*pFYMhH!oZcgc5a)WmL?;TPSrerTVPp<#s+imF3v#!FuBNNa`#6 z!GdTCF|IIpz#(eV^mrYKThA4Bnv&vQet@%v9kuRu3EHx1-2-it@E`%9#u`)HRN#M? z7aJ{wzKczn#w^`OZ>Jb898^Xxq)0zd{3Tu7+{-sge-rQ z&0PME&wIo6W&@F|%Z8@@N3)@a_ntJ#+g{pUP7i?~3FirqU`rdf8joMG^ld?(9b7Iv z>TJgBg#)(FcW)h!_if#cWBh}f+V08GKyg|$P#KTS&%=!+0a%}O${0$i)kn9@G!}En zv)_>s?glPiLbbx)xk(lD-QbY(OP3;MSXM5E*P&_`Zks2@46n|-h$Y2L7B)iH{GAAq19h5-y0q>d^oy^y+soJu9lXxAe%jcm?=pDLFEG2kla40e!5a}mpe zdL=WlZ=@U6{>g%5a+y-lx)01V-x;wh%F{=qy#XFEAqcd+m}_!lQ)-9iiOL%&G??t| z?&NSdaLqdPdbQs%y0?uIIHY7rw1EDxtQ=DU!i{)Dkn~c$LG5{rAUYM1j5*G@oVn9~ zizz{XH(nbw%f|wI=4rw^6mNIahQpB)OQy10^}ACdLPFc2@ldVi|v@1nWLND?)53O5|fg`RZW&XpF&s3@c-R?aad!$WoH6u0B|}zt)L($E^@U- zO#^fxu9}Zw7Xl~nG1FVM6DZSR0*t!4IyUeTrnp@?)Z)*!fhd3)&s(O+3D^#m#bAem zpf#*aiG_0S^ofpm@9O7j`VfLU0+{$x!u^}3!zp=XST0N@DZTp!7LEVJgqB1g{psNr za0uVmh3_9qah14@M_pi~vAZ#jc*&aSm$hCNDsuQ-zPe&*Ii#2=2gP+DP4=DY z_Y0lUsyE6yaV9)K)!oI6+*4|spx2at*30CAx~6-5kfJzQ`fN8$!lz%hz^J6GY?mVH zbYR^JZ(Pmj6@vy-&!`$5soyy-NqB^8cCT40&R@|6s@m+ZxPs=Bu77-+Os7+bsz4nA3DrJ8#{f98ZMaj-+BD;M+Jk?pgFcZIb}m9N z{ct9T)Kye&2>l^39O4Q2@b%sY?u#&O9PO4@t0c$NUXG}(DZJ<;_oe2~e==3Z1+`Zo zFrS3ns-c}ZognVBHbg#e+1JhC(Yq7==rSJQ8J~}%94(O#_-zJKwnBXihl#hUd9B_>+T& z7eHHPRC?5ONaUiCF7w|{J`bCWS7Q&xw-Sa={j-f)n5+I=9s;E#fBQB$`DDh<^mGiF zu-m_k+)dkBvBO(VMe2O4r^sf3;sk9K!xgXJU>|t9Vm8Ty;fl5pZzw z9j|}ZD}6}t;20^qrS?YVPuPRS<39d^y0#O1o_1P{tN0?OX!lc-ICcHI@2#$cY}_CY zev|xdFcRTQ_H)1fJ7S0*SpPs8e{d+9lR~IZ^~dKx!oxz?=Dp!fD`H=LH{EeC8C&z-zK$e=!5z8NL=4zx2{hl<5z*hEmO=b-7(k5H`bA~5gT30Sjy`@-_C zKM}^so9Ti1B;DovHByJkTK87cfbF16sk-G>`Q4-txyMkyQS$d}??|Aytz^;0GxvOs zPgH>h>K+`!HABVT{sYgzy3CF5ftv6hI-NRfgu613d|d1cg^jh+SK7WHWaDX~hlIJ3 z>%WxKT0|Db1N-a4r1oPKtF--^YbP=8Nw5CNt_ZnR{N(PXI>Cm$eqi@_IRmJ9#)~ZHK_UQ8mi}w^`+4$OihUGVz!kW^qxnCFo)-RIDbA&k-Y=+*xYv5y4^VQ9S)4W5Pe?_RjAX6lS6Nz#!Hry=+PKx2|o_H_3M`}Dq{Bl_PbP(qel~P@=m}VGW*pK96 zI@fVag{DZHi}>3}<(Hv<7cVfWiaVLWr@WWxk5}GDEbB<+Aj;(c>;p1qmyAIj+R!`@#jf$ zy4`q23L-72Zs4j?W+9lQD;CYIULt%;O3jPWg2a%Zs!5OW>5h1y{Qof!p&QxNt5=T( zd5fy&7=hyq;J8%86YBOdc$BbIFxJx>dUyTh`L z-oKa=OhRK9UPVRWS`o2x53bAv+py)o)kNL6 z9W1Dlk-g6Ht@-Z^#6%`9S9`909^EMj?9R^4IxssCY-hYzei^TLq7Cj>z$AJyaU5=z zl!xiWvz0U8kY$etrcp8mL;sYqGZD!Hs-U2N{A|^oEKA482v1T%cs%G@X9M?%lX)p$ zZoC7iYTPe8yxY0Jne|s)fCRe1mU=Vb1J_&WcIyP|x4$;VSVNC`M+e#oOA`#h>pyU6 z?7FeVpk`Hsu`~T3i<_4<5fu?RkhM;@LjKo6nX>pa%8dSdgPO9~Jze;5r>Tb1Xqh5q z&SEdTXevV@PT~!O6z|oypTk7Qq+BNF5IQ(8s18c=^0@sc8Gi|3e>VKCsaZ?6=rrck zl@oF5Bd0zH?@15PxSJIRroK4Wa?1o;An;p0#%ZJ^tI=(>AJ2OY0GP$E_3(+Zz4$AQ zW)QWl<4toIJ5TeF&gNXs>_rl}glkeG#GYbHHOv-G!%dJNoIKxn)FK$5&2Zv*AFic! z@2?sY&I*PSfZ8bU#c9fdIJQa_cQijnj39-+hS@+~e*5W3bj%A}%p9N@>*tCGOk+cF zlcSzI6j%Q|2e>QG3A<86w?cx6sBtLNWF6_YR?~C)IC6_10SNoZUHrCpp6f^*+*b8` zlx4ToZZuI0XW1W)24)92S)y0QZa);^NRTX6@gh8@P?^=#2dV9s4)Q@K+gnc{6|C}& zDLHr7nDOLrsH)L@Zy{C_2UrYdZ4V{|{c8&dRG;wY`u>w%$*p>PO_}3`Y21pk?8Wtq zGwIXTulf7AO2FkPyyh2TZXM1DJv>hI`}x`OzQI*MBc#=}jaua&czSkI2!s^rOci|V zFkp*Vbiz5vWa9HPFXMi=BV&n3?1?%8#1jq?p^3wAL`jgcF)7F4l<(H^!i=l-(OTDE zxf2p71^WRIExLf?ig0FRO$h~aA23s#L zuZPLkm>mDwBeIu*C7@n@_$oSDmdWY7*wI%aL73t~`Yu7YwE-hxAATmOi0dmB9|D5a zLsR7OQcA0`vN9m0L|5?qZ|jU+cx3_-K2!K$zDbJ$UinQy<9nd5ImWW5n^&=Gg>Gsh zY0u?m1e^c~Ug39M{{5q2L~ROq#c{eG8Oy#5h_q=#AJj2Yops|1C^nv0D1=fBOdfAG z%>=vl*+_w`&M7{qE#$xJJp_t>bSh7Mpc(RAvli9kk3{KgG5K@a-Ue{IbU{`umXrR3ra5Y7xiX42+Q%N&-0#`ae_ z#$Y6Wa++OPEDw@96Zz##PFo9sADepQe|hUy!Zzc2C(L`k9&=a8XFr+!hIS>D2{pdGP1SzwyaGLiH3j--P>U#TWw90t8{8Bt%m7Upspl#=*hS zhy|(XL6HOqBW}Og^tLX7 z+`b^L{O&oqjwbxDDTg2B;Yh2(fW>%S5Pg8^u1p*EFb z`(fbUM0`afawYt%VBfD&b3MNJ39~Ldc@SAuzsMiN%E}5{uUUBc7hc1IUE~t-Y9h@e7PC|sv$xGx=hZiMXNJxz5V(np%6u{n24iWX#!8t#>Ob$in<>dw96H)oGdTHnU zSM+BPss*5)Wz@+FkooMxxXZP1{2Nz7a6BB~-A_(c&OiM)UUNoa@J8FGxtr$)`9;|O z(Q?lq1Q+!E`}d?KemgC!{nB1JJ!B>6J@XGQp9NeQvtbM2n7F%v|IS=XWPVZY(>oq$ zf=}8O_x`KOxZoGnp=y24x}k6?gl_0dTF!M!T`={`Ii{GnT1jrG9gPh)R=RZG8lIR| z{ZJ6`x8n|y+lZuy${fuEDTAf`OP!tGySLXD}ATJO5UoZv|Xo3%7O~L63+kw}v)Ci=&tWx3bQJfL@5O18CbPlkR^IcKA zy1=^Vl-K-QBP?9^R`@;czcUw;Enbbyk@vJQB>BZ4?;DM%BUf^eZE+sOy>a){qCY6Y znYy;KGpch-zf=5|p#SoAV+ie8M5(Xg-{FoLx-wZC9IutT!(9rJ8}=!$!h%!J+vE2e z(sURwqCC35v?1>C1L)swfA^sr16{yj7-zbT6Rf26-JoEt%U?+|rQ zeBuGohE?@*!zR9)1P|3>KmJSgK*fOt>N>j}LJB`>o(G#Dduvx7@DY7};W7K;Yj|8O zGF<+gTuoIKe7Rf+LQG3-V1L^|E;F*}bQ-{kuHq}| ze_NwA7~US19sAZ)@a`g*zkl*ykv2v3tPrb4Og2#?k6Lc7@1I~+ew48N&03hW^1Cx+ zfk5Lr4-n=#HYg<7ka5i>2A@ZeJ60gl)IDX!!p zzfXZQ?GrT>JEKl7$SH!otzK6=0dIlqN)c23YLB&Krf9v-{@V8p+-e2`ujFR!^M%*; ze_7(Jh$QgoqwB!HbX=S+^wqO15O_TQ0-qX8f-|&SOuo3ZE{{9Jw5{}>MhY}|GBhO& zv48s_B=9aYQfa;d>~1Z$y^oUUaDer>7ve5+Gf?rIG4GZ!hRKERlRNgg_C{W_!3tsI2TWbX8f~MY)1Q`6Wj&JJ~*;ay_0@e zzx+mE-pu8{cEcVfBqsnm=jFU?H}xj@%CAx#NO>3 z_re3Rq%d1Y7VkKy{=S73&p;4^Praw6Y59VCP6M?!Kt7{v#DG#tz?E)`K95gH_mEvb z%$<~_mQ$ad?~&T=O0i0?`YSp?E3Dj?V>n+uTRHAXn`l!pH9Mr}^D1d@mkf+;(tV45 zH_yfs^kOGLXlN*0GU;O&{=awxd?&`{JPRr$z<1HcAO2K`K}92$wC}ky&>;L?#!(`w z68avZGvb728!vgw>;8Z8I@mLtI`?^u6R>sK4E7%=y)jpmE$fH!Dj*~(dy~-2A5Cm{ zl{1AZw`jaDmfvaB?jvKwz!GC}@-Dz|bFm1OaPw(ia#?>vF7Y5oh{NVbyD~cHB1KFn z9C@f~X*Wk3>sQH9#D~rLPslAd26@AzMh=_NkH_yTNXx6-AdbAb z{Ul89YPHslD?xAGzOlQ*aMYUl6#efCT~WI zOvyiewT=~l1W(_2cEd(8rDywOwjM-7P9!8GCL-1<9KXXO=6%!9=W++*l1L~gRSxLVd8K=A7&t52ql=J&BMQu{fa6y zXO_e>d?4X)xp2V8e3xIQGbq@+vo#&n>-_WreTTW0Yr?|YRPP43cDYACMQ(3t6(?_k zfgDOAU^-pew_f5U#WxRXB30wcfDS3;k~t@b@w^GG&<5n$Ku?tT(%bQH(@UHQGN)N|nfC~7?(etU`}XB)$>KY;s=bYGY#kD%i9fz= z2nN9l?UPMKYwn9bX*^xX8Y@%LNPFU>s#Ea1DaP%bSioqRWi9JS28suTdJycYQ+tW7 zrQ@@=13`HS*dVKaVgcem-45+buD{B;mUbY$YYULhxK)T{S?EB<8^YTP$}DA{(&)@S zS#<8S96y9K2!lG^VW-+CkfXJIH;Vo6wh)N}!08bM$I7KEW{F6tqEQ?H@(U zAqfi%KCe}2NUXALo;UN&k$rU0BLNC$24T_mcNY(a@lxR`kqNQ0z%8m>`&1ro40HX} z{{3YQ;2F9JnVTvDY<4)x+88i@MtXE6TBd7POk&QfKU-F&*C`isS(T_Q@}K)=zW#K@ zbXpcAkTT-T5k}Wj$dMZl7=GvlcCMt}U`#Oon1QdPq%>9J$rKTY8#OmlnNWBYwafhx zqFnym@okL#Xw>4SeRFejBnZzY$jbO)e^&&sHBgMP%Ygfi!9_3hp17=AwLBNFTimf0 zw6BHNXw19Jg_Ud6`5n#gMpqe%9!QB^_7wAYv8nrW94A{*t8XZu0UT&`ZHfkd(F{Px zD&NbRJP#RX<=+sEeGs2`9_*J2OlECpR;4uJie-d__m*(aaGE}HIo+3P{my@;a~9Y$ zHBXVJ83#&@o6{M+pE9^lI<4meLLFN_3rwgR4IRyp)~OF0n+#ORrcJ2_On9-78bWbG zuCO0esc*n1X3@p1?lN{qWS?l7J$^jbpeel{w~51*0CM+q9@9X=>%MF(ce~om(}?td zjkUmdUR@LOn-~6LX#=@a%rvj&>DFEoQscOvvC@&ZB5jVZ-;XzAshwx$;Qf@U41W=q zOSSjQGQV8Qi3*4DngNMIM&Cxm7z*-K`~Bl(TcEUxjQ1c=?)?wF8W1g;bAR%sM#LK( z_Op?=P%)Z+J!>vpN`By0$?B~Out%P}kCriDq@}In&fa_ZyKV+nLM0E?hfxuu%ciUz z>yAk}OydbWNl7{)#112j&qmw;*Uj&B;>|;Qwfc?5wIYIHH}s6Mve@5c5r+y)jK9i( z_}@uC(98g)==AGkVN?4>o@w=7x9qhW^ zB(b5%%4cHSV?3M?k&^py)j*LK16T^Ef4tb05-h-tyrjt$5!oo4spEfXFK7r_Gfv7#x$bsR7T zs;dqxzUg9v&GjsQGKTP*=B(;)be2aN+6>IUz+Hhw-n>^|`^xu*xvjGPaDoFh2W4-n z@Wji{5Y$m>@Vt7TE_QVQN4*vcfWv5VY-dT0SV=l=8LAEq1go*f zkjukaDV=3kMAX6GAf0QOQHwP^{Z^=#Lc)sh`QB)Ftl&31jABvq?8!3bt7#8vxB z53M{4{GR4Hl~;W3r}PgXSNOt477cO62Yj(HcK&30zsmWpvAplCtpp&mC{`2Ue*Bwu zF&UX1;w%`Bs1u%RtGPFl=&sHu@Q1nT`z={;5^c^^S~^?2-?<|F9RT*KQmfgF!7=wD@hytxbD;=9L6PZrK*1<4HMObNWehA62DtTy)q5H|57 z9dePuC!1;0MMRRl!S@VJ8qG=v^~aEU+}2Qx``h1LII!y{crP2ky*R;Cb;g|r<#ryo zju#s4dE?5CTIZKc*O4^3qWflsQ(voX>(*_JP7>Q&$%zCAIBTtKC^JUi@&l6u&t0hXMXjz_y!;r@?k|OU9aD%938^TZ>V? zqJmom_6dz4DBb4Cgs_Ef@}F%+cRCR%UMa9pi<-KHN;t#O@cA%(LO1Rb=h?5jiTs93 zPLR78p+3t>z4|j=<>2i4b`ketv}9Ax#B0)hn7@bFl;rDfP8p7u9XcEb!5*PLKB(s7wQC2kzI^@ae)|DhNDmSy1bOLid%iIap@24A(q2XI!z_hkl-$1T10 z+KKugG4-}@u8(P^S3PW4x>an;XWEF-R^gB{`t8EiP{ZtAzoZ!JRuMRS__-Gg#Qa3{<;l__CgsF+nfmFNi}p z>rV!Y6B@cC>1up)KvaEQiAvQF!D>GCb+WZsGHjDeWFz?WVAHP65aIA8u6j6H35XNYlyy8>;cWe3ekr};b;$9)0G`zsc9LNsQ&D?hvuHRpBxH)r-1t9|Stc*u<}Ol&2N+wPMom}d15_TA=Aprp zjN-X3*Af$7cDWMWp##kOH|t;c2Pa9Ml4-)o~+7P;&q8teF-l}(Jt zTGKOQqJTeT!L4d}Qw~O0aanA$Vn9Rocp-MO4l*HK)t%hcp@3k0%&_*wwpKD6ThM)R z8k}&7?)YS1ZYKMiy?mn>VXiuzX7$Ixf7EW8+C4K^)m&eLYl%#T=MC;YPvD&w#$MMf zQ=>`@rh&&r!@X&v%ZlLF42L_c=5dSU^uymKVB>5O?AouR3vGv@ei%Z|GX5v1GK2R* zi!!}?+-8>J$JH^fPu@)E6(}9$d&9-j51T^n-e0Ze%Q^)lxuex$IL^XJ&K2oi`wG}QVGk2a7vC4X?+o^z zsCK*7`EUfSuQA*K@Plsi;)2GrayQOG9OYF82Hc@6aNN5ulqs1Of-(iZQdBI^U5of^ zZg2g=Xtad7$hfYu6l~KDQ}EU;oIj(3nO#u9PDz=eO3(iax7OCmgT2p_7&^3q zg7aQ;Vpng*)kb6=sd5?%j5Dm|HczSChMo8HHq_L8R;BR5<~DVyU$8*Tk5}g0eW5x7 z%d)JFZ{(Y<#OTKLBA1fwLM*fH7Q~7Sc2Ne;mVWqt-*o<;| z^1@vo_KTYaMnO$7fbLL+qh#R$9bvnpJ$RAqG+z8h|} z3F5iwG*(sCn9Qbyg@t0&G}3fE0jGq3J!JmG2K&$urx^$z95) z7h?;4vE4W=v)uZ*Eg3M^6f~|0&T)2D;f+L_?M*21-I1pnK(pT$5l#QNlT`SidYw~o z{`)G)Asv#cue)Ax1RNWiRUQ(tQ(bzd-f2U4xlJK+)ZWBxdq#fp=A>+Qc%-tl(c)`t z$e2Ng;Rjvnbu7((;v4LF9Y1?0el9hi!g>G{^37{ z`^s-03Z5jlnD%#Mix19zkU_OS|86^_x4<0(*YbPN}mi-$L?Z4K(M|2&VV*n*ZYN_UqI?eKZi3!b)i z%n3dzUPMc-dc|q}TzvPy!VqsEWCZL(-eURDRG4+;Eu!LugSSI4Fq$Ji$Dp08`pfP_C5Yx~`YKcywlMG;$F z)R5!kVml_Wv6MSpeXjG#g?kJ0t_MEgbXlUN3k|JJ%N>|2xn8yN>>4qxh!?dGI}s|Y zDTKd^JCrRSN+%w%D_uf=Tj6wIV$c*g8D96jb^Kc#>5Fe-XxKC@!pIJw0^zu;`_yeb zhUEm-G*C=F+jW%cP(**b61fTmPn2WllBr4SWNdKe*P8VabZsh0-R|?DO=0x`4_QY) zR7sthW^*BofW7{Sak&S1JdiG?e=SfL24Y#w_)xrBVhGB-13q$>mFU|wd9Xqe-o3{6 zSn@@1@&^)M$rxb>UmFuC+pkio#T;mSnroMVZJ%nZ!uImi?%KsIX#@JU2VY(`kGb1A z7+1MEG)wd@)m^R|a2rXeviv$!emwcY(O|M*xV!9%tBzarBOG<4%gI9SW;Um_gth4=gznYzOFd)y8e+3APCkL)i-OI`;@7-mCJgE`js(M} z;~ZcW{{FMVVO)W>VZ}ILouF#lWGb%Couu}TI4kubUUclW@jEn6B_^v!Ym*(T*4HF9 zWhNKi8%sS~viSdBtnrq!-Dc5(G^XmR>DFx8jhWvR%*8!m*b*R8e1+`7{%FACAK`7 zzdy8TmBh?FVZ0vtw6npnWwM~XjF2fNvV#ZlGG z?FxHkXHN>JqrBYoPo$)zNC7|XrQfcqmEXWud~{j?La6@kbHG@W{xsa~l1=%eLly8B z4gCIH05&Y;6O2uFSopNqP|<$ml$N40^ikxw0`o<~ywS1(qKqQN!@?Ykl|bE4M?P+e zo$^Vs_+x)iuw?^>>`$&lOQOUkZ5>+OLnRA)FqgpDjW&q*WAe(_mAT6IKS9;iZBl8M z<@=Y%zcQUaSBdrs27bVK`c$)h6A1GYPS$y(FLRD5Yl8E3j0KyH08#8qLrsc_qlws; znMV%Zq8k+&T2kf%6ZO^2=AE9>?a587g%-={X}IS~P*I(NeCF9_9&`)|ok0iiIun zo+^odT0&Z4k;rn7I1v87=z!zKU(%gfB$(1mrRYeO$sbqM22Kq68z9wgdg8HBxp>_< zn9o%`f?sVO=IN#5jSX&CGODWlZfQ9A)njK2O{JutYwRZ?n0G_p&*uwpE`Md$iQxrd zoQfF^b8Ou)+3BO_3_K5y*~?<(BF@1l+@?Z6;^;U>qlB)cdro;rxOS1M{Az$s^9o5sXDCg8yD<=(pKI*0e zLk>@lo#&s0)^*Q+G)g}C0IErqfa9VbL*Qe=OT@&+N8m|GJF7jd83vY#SsuEv2s{Q> z>IpoubNs>D_5?|kXGAPgF@mb_9<%hjU;S0C8idI)a=F#lPLuQJ^7OnjJlH_Sks9JD zMl1td%YsWq3YWhc;E$H1<0P$YbSTqs`JKY%(}svsifz|h8BHguL82dBl+z0^YvWk8 zGy;7Z0v5_FJ2A$P0wIr)lD?cPR%cz>kde!=W%Ta^ih+Dh4UKdf7ip?rBz@%y2&>`6 zM#q{JXvW9ZlaSk1oD!n}kSmcDa2v6T^Y-dy+#fW^y>eS8_%<7tWXUp8U@s$^{JFfKMjDAvR z$YmVB;n3ofl!ro9RNT!TpQpcycXCR}$9k5>IPWDXEenQ58os?_weccrT+Bh5sLoiH zZ_7~%t(vT)ZTEO= zb0}@KaD{&IyK_sd8b$`Qz3%UA`nSo zn``!BdCeN!#^G;lK@G2ron*0jQhbdw)%m$2;}le@z~PSLnU-z@tL)^(p%P>OO^*Ff zNRR9oQ`W+x^+EU+3BpluwK77|B3=8QyT|$V;02bn_LF&3LhLA<#}{{)jE)}CiW%VEU~9)SW+=F%7U-iYlQ&q!#N zwI2{(h|Pi&<8_fqvT*}FLN^0CxN}#|3I9G_xmVg$gbn2ZdhbmGk7Q5Q2Tm*ox8NMo zv`iaZW|ZEOMyQga5fts?&T-eCCC9pS0mj7v0SDkD=*^MxurP@89v&Z#3q{FM!a_nr zb?KzMv`BBFOew>4!ft@A&(v-kWXny-j#egKef|#!+3>26Qq0 zv!~8ev4G`7Qk>V1TaMT-&ziqoY3IJp8_S*%^1j73D|=9&;tDZH^!LYFMmME4*Wj(S zRt~Q{aLb_O;wi4u&=}OYuj}Lw*j$@z*3>4&W{)O-oi@9NqdoU!=U%d|se&h?^$Ip# z)BY+(1+cwJz!yy4%l(aLC;T!~Ci>yAtXJb~b*yr&v7f{YCU8P|N1v~H`xmGsG)g)y z4%mv=cPd`s7a*#OR7f0lpD$ueP>w8qXj0J&*7xX+U!uat5QNk>zwU$0acn5p=$88L=jn_QCSYkTV;1~(yUem#0gB`FeqY98sf=>^@ z_MCdvylv~WL%y_%y_FE1)j;{Szj1+K7Lr_y=V+U zk6Tr;>XEqlEom~QGL!a+wOf(@ZWoxE<$^qHYl*H1a~kk^BLPn785%nQb$o;Cuz0h& za9LMx^bKEbPS%e8NM33Jr|1T|ELC(iE!FUci38xW_Y7kdHid#2ie+XZhP;2!Z;ZAM zB_cXKm)VrPK!SK|PY00Phwrpd+x0_Aa;}cDQvWKrwnQrqz##_gvHX2ja?#_{f#;bz`i>C^^ zTLDy;6@HZ~XQi7rph!mz9k!m;KchA)uMd`RK4WLK7)5Rl48m#l>b(#`WPsl<0j z-sFkSF6>Nk|LKnHtZ`W_NnxZP62&w)S(aBmmjMDKzF%G;3Y?FUbo?>b5;0j8Lhtc4 zr*8d5Y9>g@FFZaViw7c16VsHcy0u7M%6>cG1=s=Dtx?xMJSKIu9b6GU8$uSzf43Y3 zYq|U+IWfH;SM~*N1v`KJo!|yfLxTFS?oHsr3qvzeVndVV^%BWmW6re_S!2;g<|Oao z+N`m#*i!)R%i1~NO-xo{qpwL0ZrL7hli;S z3L0lQ_z}z`fdK39Mg~Zd*%mBdD;&5EXa~@H(!###L`ycr7gW`f)KRuqyHL3|uyy3h zSS^td#E&Knc$?dXs*{EnPYOp^-vjAc-h4z#XkbG&REC7;0>z^^Z}i8MxGKerEY z>l?(wReOlXEsNE5!DO&ZWyxY)gG#FSZs%fXuzA~XIAPVp-%yb2XLSV{1nH6{)5opg z(dZKckn}Q4Li-e=eUDs1Psg~5zdn1>ql(*(nn6)iD*OcVkwmKL(A{fix(JhcVB&}V zVt*Xb!{gzvV}dc446>(D=SzfCu7KB`oMjv6kPzSv&B>>HLSJP|wN`H;>oRw*tl#N) z*zZ-xwM7D*AIsBfgqOjY1Mp9aq$kRa^dZU_xw~KxP;|q(m+@e+YSn~`wEJzM|Ippb zzb@%;hB7iH4op9SqmX?j!KP2chsb79(mFossBO-Zj8~L}9L%R%Bw<`^X>hjkCY5SG z7lY!8I2mB#z)1o;*3U$G)3o0A&{0}#B;(zPd2`OF`Gt~8;0Re8nIseU z_yzlf$l+*-wT~_-cYk$^wTJ@~7i@u(CZs9FVkJCru<*yK8&>g+t*!JqCN6RH%8S-P zxH8+Cy#W?!;r?cLMC(^BtAt#xPNnwboI*xWw#T|IW^@3|q&QYY6Ehxoh@^URylR|T zne-Y6ugE^7p5bkRDWIh)?JH5V^ub82l-LuVjDr7UT^g`q4dB&mBFRWGL_C?hoeL(% zo}ocH5t7|1Mda}T!^{Qt9vmA2ep4)dQSZO>?Eq8}qRp&ZJ?-`Tnw+MG(eDswP(L*X3ahC2Ad0_wD^ff9hfzb%Jd`IXx5 zae@NMzBXJDwJS?7_%!TB^E$N8pvhOHDK$7YiOelTY`6KX8hK6YyT$tk*adwN>s^Kp zwM3wGVPhwKU*Yq-*BCs}l`l#Tej(NQ>jg*S0TN%D+GcF<14Ms6J`*yMY;W<-mMN&-K>((+P}+t+#0KPGrzjP zJ~)=Bcz%-K!L5ozIWqO(LM)l_9lVOc4*S65&DKM#TqsiWNG{(EZQw!bc>qLW`=>p-gVJ;T~aN2D_- z{>SZC=_F+%hNmH6ub%Ykih0&YWB!%sd%W5 zHC2%QMP~xJgt4>%bU>%6&uaDtSD?;Usm}ari0^fcMhi_)JZgb1g5j zFl4`FQ*%ROfYI}e7RIq^&^a>jZF23{WB`T>+VIxj%~A-|m=J7Va9FxXV^%UwccSZd zuWINc-g|d6G5;95*%{e;9S(=%yngpfy+7ao|M7S|Jb0-4+^_q-uIqVS&ufU880UDH*>(c)#lt2j zzvIEN>>$Y(PeALC-D?5JfH_j+O-KWGR)TKunsRYKLgk7eu4C{iF^hqSz-bx5^{z0h ze2+u>Iq0J4?)jIo)}V!!m)%)B;a;UfoJ>VRQ*22+ncpe9f4L``?v9PH&;5j{WF?S_C>Lq>nkChZB zjF8(*v0c(lU^ZI-)_uGZnnVRosrO4`YinzI-RSS-YwjYh3M`ch#(QMNw*)~Et7Qpy z{d<3$4FUAKILq9cCZpjvKG#yD%-juhMj>7xIO&;c>_7qJ%Ae8Z^m)g!taK#YOW3B0 zKKSMOd?~G4h}lrZbtPk)n*iOC1~mDhASGZ@N{G|dF|Q^@1ljhe=>;wusA&NvY*w%~ zl+R6B^1yZiF)YN>0ms%}qz-^U-HVyiN3R9k1q4)XgDj#qY4CE0)52%evvrrOc898^ z*^)XFR?W%g0@?|6Mxo1ZBp%(XNv_RD-<#b^?-Fs+NL^EUW=iV|+Vy*F%;rBz~pN7%-698U-VMfGEVnmEz7fL1p)-5sLT zL;Iz>FCLM$p$c}g^tbkGK1G$IALq1Gd|We@&TtW!?4C7x4l*=4oF&&sr0Hu`x<5!m zhX&&Iyjr?AkNXU_5P_b^Q3U9sy#f6ZF@2C96$>1k*E-E%DjwvA{VL0PdU~suN~DZo zm{T!>sRdp`Ldpp9olrH@(J$QyGq!?#o1bUo=XP2OEuT3`XzI>s^0P{manUaE4pI%! zclQq;lbT;nx7v3tR9U)G39h?ryrxzd0xq4KX7nO?piJZbzT_CU&O=T(Vt;>jm?MgC z2vUL#*`UcMsx%w#vvjdamHhmN!(y-hr~byCA-*iCD};#l+bq;gkwQ0oN=AyOf@8ow>Pj<*A~2*dyjK}eYdN);%!t1 z6Y=|cuEv-|5BhA?n2Db@4s%y~(%Wse4&JXw=HiO48%c6LB~Z0SL1(k^9y?ax%oj~l zf7(`iAYLdPRq*ztFC z7VtAb@s{as%&Y;&WnyYl+6Wm$ru*u!MKIg_@01od-iQft0rMjIj8e7P9eKvFnx_X5 zd%pDg-|8<>T2Jdqw>AII+fe?CgP+fL(m0&U??QL8YzSjV{SFi^vW~;wN@or_(q<0Y zRt~L}#JRcHOvm$CB)T1;;7U>m%)QYBLTR)KTARw%zoDxgssu5#v{UEVIa<>{8dtkm zXgbCGp$tfue+}#SD-PgiNT{Zu^YA9;4BnM(wZ9-biRo_7pN}=aaimjYgC=;9@g%6< zxol5sT_$<8{LiJ6{l1+sV)Z_QdbsfEAEMw!5*zz6)Yop?T0DMtR_~wfta)E6_G@k# zZRP11D}$ir<`IQ`<(kGfAS?O-DzCyuzBq6dxGTNNTK?r^?zT30mLY!kQ=o~Hv*k^w zvq!LBjW=zzIi%UF@?!g9vt1CqdwV(-2LYy2=E@Z?B}JDyVkluHtzGsWuI1W5svX~K z&?UJ45$R7g>&}SFnLnmw09R2tUgmr_w6mM9C}8GvQX>nL&5R#xBqnp~Se(I>R42`T zqZe9p6G(VzNB3QD><8+y%{e%6)sZDRXTR|MI zM#eZmao-~_`N|>Yf;a;7yvd_auTG#B?Vz5D1AHx=zpVUFe7*hME z+>KH5h1In8hsVhrstc>y0Q!FHR)hzgl+*Q&5hU9BVJlNGRkXiS&06eOBV^dz3;4d5 zeYX%$62dNOprZV$px~#h1RH?_E%oD6y;J;pF%~y8M)8pQ0olYKj6 zE+hd|7oY3ot=j9ZZ))^CCPADL6Jw%)F@A{*coMApcA$7fZ{T@3;WOQ352F~q6`Mgi z$RI6$8)a`Aaxy<8Bc;{wlDA%*%(msBh*xy$L-cBJvQ8hj#FCyT^%+Phw1~PaqyDou^JR0rxDkSrmAdjeYDFDZ`E z)G3>XtpaSPDlydd$RGHg;#4|4{aP5c_Om z2u5xgnhnA)K%8iU==}AxPxZCYC)lyOlj9as#`5hZ=<6<&DB%i_XCnt5=pjh?iusH$ z>)E`@HNZcAG&RW3Ys@`Ci{;8PNzE-ZsPw$~Wa!cP$ye+X6;9ceE}ah+3VY7Mx}#0x zbqYa}eO*FceiY2jNS&2cH9Y}(;U<^^cWC5Ob&)dZedvZA9HewU3R;gRQ)}hUdf+~Q zS_^4ds*W1T#bxS?%RH&<739q*n<6o|mV;*|1s>ly-Biu<2*{!!0#{_234&9byvn0* z5=>{95Zfb{(?h_Jk#ocR$FZ78O*UTOxld~0UF!kyGM|nH%B*qf)Jy}N!uT9NGeM19 z-@=&Y0yGGo_dw!FD>juk%P$6$qJkj}TwLBoefi;N-$9LAeV|)|-ET&culW9Sb_pc_ zp{cXI0>I0Jm_i$nSvGnYeLSSj{ccVS2wyL&0x~&5v;3Itc82 z5lIAkfn~wcY-bQB$G!ufWt%qO;P%&2B_R5UKwYxMemIaFm)qF1rA zc>gEihb=jBtsXCi0T%J37s&kt*3$s7|6)L(%UiY)6axuk{6RWIS8^+u;)6!R?Sgap z9|6<0bx~AgVi|*;zL@2x>Pbt2Bz*uv4x-`{F)XatTs`S>unZ#P^ZiyjpfL_q2z^fqgR-fbOcG=Y$q>ozkw1T6dH8-)&ww+z?E0 zR|rV(9bi6zpX3Ub>PrPK!{X>e$C66qCXAeFm)Y+lX8n2Olt7PNs*1^si)j!QmFV#t z0P2fyf$N^!dyTot&`Ew5{i5u<8D`8U`qs(KqaWq5iOF3x2!-z65-|HsyYz(MAKZ?< zCpQR;E)wn%s|&q(LVm0Ab>gdmCFJeKwVTnv@Js%!At;I=A>h=l=p^&<4;Boc{$@h< z38v`3&2wJtka@M}GS%9!+SpJ}sdtoYzMevVbnH+d_eMxN@~~ zZq@k)7V5f8u!yAX2qF3qjS7g%n$JuGrMhQF!&S^7(%Y{rP*w2FWj(v_J{+Hg*}wdWOd~pHQ19&n3RWeljK9W%sz&Y3Tm3 zR`>6YR54%qBHGa)2xbs`9cs_EsNHxsfraEgZ)?vrtooeA0sPKJK7an){ngtV@{SBa zkO6ORr1_Xqp+`a0e}sC*_y(|RKS13ikmHp3C^XkE@&wjbGWrt^INg^9lDz#B;bHiW zkK4{|cg08b!yHFSgPca5)vF&gqCgeu+c82%&FeM^Bb}GUxLy-zo)}N;#U?sJ2?G2BNe*9u_7kE5JeY!it=f`A_4gV3} z`M!HXZy#gN-wS!HvHRqpCHUmjiM;rVvpkC!voImG%OFVN3k(QG@X%e``VJSJ@Z7tb z*Onlf>z^D+&$0!4`IE$;2-NSO9HQWd+UFW(r;4hh;(j^p4H-~6OE!HQp^96v?{9Zt z;@!ZcccV%C2s6FMP#qvo4kG6C04A>XILt>JW}%0oE&HM5f6 zYLD!;My>CW+j<~=Wzev{aYtx2ZNw|ptTFV(4;9`6Tmbz6K1)fv4qPXa2mtoPt&c?P zhmO+*o8uP3ykL6E$il00@TDf6tOW7fmo?Oz_6GU^+5J=c22bWyuH#aNj!tT-^IHrJ zu{aqTYw@q;&$xDE*_kl50Jb*dp`(-^p={z}`rqECTi~3 z>0~A7L6X)=L5p#~$V}gxazgGT7$3`?a)zen>?TvAuQ+KAIAJ-s_v}O6@`h9n-sZk> z`3{IJeb2qu9w=P*@q>iC`5wea`KxCxrx{>(4{5P+!cPg|pn~;n@DiZ0Y>;k5mnKeS z!LIfT4{Lgd=MeysR5YiQKCeNhUQ;Os1kAymg6R!u?j%LF z4orCszIq_n52ulpes{(QN|zirdtBsc{9^Z72Ycb2ht?G^opkT_#|4$wa9`)8k3ilU z%ntAi`nakS1r10;#k^{-ZGOD&Z2|k=p40hRh5D7(&JG#Cty|ECOvwsSHkkSa)36$4 z?;v#%@D(=Raw(HP5s>#4Bm?f~n1@ebH}2tv#7-0l-i^H#H{PC|F@xeNS+Yw{F-&wH z07)bj8MaE6`|6NoqKM~`4%X> zKFl&7g1$Z3HB>lxn$J`P`6GSb6CE6_^NA1V%=*`5O!zP$a7Vq)IwJAki~XBLf=4TF zPYSL}>4nOGZ`fyHChq)jy-f{PKFp6$plHB2=;|>%Z^%)ecVue(*mf>EH_uO^+_zm? zJATFa9SF~tFwR#&0xO{LLf~@}s_xvCPU8TwIJgBs%FFzjm`u?1699RTui;O$rrR{# z1^MqMl5&6)G%@_k*$U5Kxq84!AdtbZ!@8FslBML}<`(Jr zenXrC6bFJP=R^FMBg7P?Pww-!a%G@kJH_zezKvuWU0>m1uyy}#Vf<$>u?Vzo3}@O% z1JR`B?~Tx2)Oa|{DQ_)y9=oY%haj!80GNHw3~qazgU-{|q+Bl~H94J!a%8UR?XsZ@ z0*ZyQugyru`V9b(0OrJOKISfi89bSVR zQy<+i_1XY}4>|D%X_`IKZUPz6=TDb)t1mC9eg(Z=tv zq@|r37AQM6A%H%GaH3szv1L^ku~H%5_V*fv$UvHl*yN4iaqWa69T2G8J2f3kxc7UE zOia@p0YNu_q-IbT%RwOi*|V|&)e5B-u>4=&n@`|WzH}BK4?33IPpXJg%`b=dr_`hU z8JibW_3&#uIN_#D&hX<)x(__jUT&lIH$!txEC@cXv$7yB&Rgu){M`9a`*PH} zRcU)pMWI2O?x;?hzR{WdzKt^;_pVGJAKKd)F$h;q=Vw$MP1XSd<;Mu;EU5ffyKIg+ z&n-Nb?h-ERN7(fix`htopPIba?0Gd^y(4EHvfF_KU<4RpN0PgVxt%7Yo99X*Pe|zR z?ytK&5qaZ$0KSS$3ZNS$$k}y(2(rCl=cuYZg{9L?KVgs~{?5adxS))Upm?LDo||`H zV)$`FF3icFmxcQshXX*1k*w3O+NjBR-AuE70=UYM*7>t|I-oix=bzDwp2*RoIwBp@r&vZukG; zyi-2zdyWJ3+E?{%?>e2Ivk`fAn&Ho(KhGSVE4C-zxM-!j01b~mTr>J|5={PrZHOgO zw@ND3=z(J7D>&C7aw{zT>GHhL2BmUX0GLt^=31RRPSnjoUO9LYzh_yegyPoAKhAQE z>#~O27dR4&LdQiak6={9_{LN}Z>;kyVYKH^d^*!`JVSXJlx#&r4>VnP$zb{XoTb=> zZsLvh>keP3fkLTIDdpf-@(ADfq4=@X=&n>dyU0%dwD{zsjCWc;r`-e~X$Q3NTz_TJ zOXG|LMQQIjGXY3o5tBm9>k6y<6XNO<=9H@IXF;63rzsC=-VuS*$E{|L_i;lZmHOD< zY92;>4spdeRn4L6pY4oUKZG<~+8U-q7ZvNOtW0i*6Q?H`9#U3M*k#4J;ek(MwF02x zUo1wgq9o6XG#W^mxl>pAD)Ll-V5BNsdVQ&+QS0+K+?H-gIBJ-ccB1=M_hxB6qcf`C zJ?!q!J4`kLhAMry4&a_0}up{CFevcjBl|N(uDM^N5#@&-nQt2>z*U}eJGi}m5f}l|IRVj-Q;a>wcLpK5RRWJ> zysdd$)Nv0tS?b~bw1=gvz3L_ZAIdDDPj)y|bp1;LE`!av!rODs-tlc}J#?erTgXRX z$@ph%*~_wr^bQYHM7<7=Q=45v|Hk7T=mDpW@OwRy3A_v`ou@JX5h!VI*e((v*5Aq3 zVYfB4<&^Dq5%^?~)NcojqK`(VXP$`#w+&VhQOn%;4pCkz;NEH6-FPHTQ+7I&JE1+Ozq-g43AEZV>ceQ^9PCx zZG@OlEF~!Lq@5dttlr%+gNjRyMwJdJU(6W_KpuVnd{3Yle(-p#6erIRc${l&qx$HA z89&sp=rT7MJ=DuTL1<5{)wtUfpPA|Gr6Q2T*=%2RFm@jyo@`@^*{5{lFPgv>84|pv z%y{|cVNz&`9C*cUely>-PRL)lHVErAKPO!NQ3<&l5(>Vp(MuJnrOf^4qpIa!o3D7( z1bjn#Vv$#or|s7Hct5D@%;@48mM%ISY7>7@ft8f?q~{s)@BqGiupoK1BAg?PyaDQ1 z`YT8{0Vz{zBwJ={I4)#ny{RP{K1dqzAaQN_aaFC%Z>OZ|^VhhautjDavGtsQwx@WH zr|1UKk^+X~S*RjCY_HN!=Jx>b6J8`Q(l4y|mc<6jnkHVng^Wk(A13-;AhawATsmmE#H%|8h}f1frs2x@Fwa_|ea+$tdG2Pz{7 z!ox^w^>^Cv4e{Xo7EQ7bxCe8U+LZG<_e$RnR?p3t?s^1Mb!ieB z#@45r*PTc_yjh#P=O8Zogo+>1#|a2nJvhOjIqKK1U&6P)O%5s~M;99O<|Y9zomWTL z666lK^QW`)cXV_^Y05yQZH3IRCW%25BHAM$c0>w`x!jh^15Zp6xYb!LoQ zr+RukTw0X2mxN%K0%=8|JHiaA3pg5+GMfze%9o5^#upx0M?G9$+P^DTx7~qq9$Qoi zV$o)yy zuUq>3c{_q+HA5OhdN*@*RkxRuD>Bi{Ttv_hyaaB;XhB%mJ2Cb{yL;{Zu@l{N?!GKE7es6_9J{9 zO(tmc0ra2;@oC%SS-8|D=omQ$-Dj>S)Utkthh{ovD3I%k}HoranSepC_yco2Q8 zY{tAuPIhD{X`KbhQIr%!t+GeH%L%q&p z3P%<-S0YY2Emjc~Gb?!su85}h_qdu5XN2XJUM}X1k^!GbwuUPT(b$Ez#LkG6KEWQB z7R&IF4srHe$g2R-SB;inW9T{@+W+~wi7VQd?}7||zi!&V^~o0kM^aby7YE_-B63^d zf_uo8#&C77HBautt_YH%v6!Q>H?}(0@4pv>cM6_7dHJ)5JdyV0Phi!)vz}dv{*n;t zf(+#Hdr=f8DbJqbMez)(n>@QT+amJ7g&w6vZ-vG^H1v~aZqG~u!1D(O+jVAG0EQ*aIsr*bsBdbD`)i^FNJ z&B@yxqPFCRGT#}@dmu-{0vp47xk(`xNM6E=7QZ5{tg6}#zFrd8Pb_bFg7XP{FsYP8 zbvWqG6#jfg*4gvY9!gJxJ3l2UjP}+#QMB(*(?Y&Q4PO`EknE&Cb~Yb@lCbk;-KY)n zzbjS~W5KZ3FV%y>S#$9Sqi$FIBCw`GfPDP|G=|y32VV-g@a1D&@%_oAbB@cAUx#aZ zlAPTJ{iz#Qda8(aNZE&0q+8r3&z_Ln)b=5a%U|OEcc3h1f&8?{b8ErEbilrun}mh3 z$1o^$-XzIiH|iGoJA`w`o|?w3m*NX|sd$`Mt+f*!hyJvQ2fS*&!SYn^On-M|pHGlu z4SC5bM7f6BAkUhGuN*w`97LLkbCx=p@K5RL2p>YpDtf{WTD|d3ucb6iVZ-*DRtoEA zCC5(x)&e=giR_id>5bE^l%Mxx>0@FskpCD4oq@%-Fg$8IcdRwkfn;DsjoX(v;mt3d z_4Mnf#Ft4x!bY!7Hz?RRMq9;5FzugD(sbt4up~6j?-or+ch~y_PqrM2hhTToJjR_~ z)E1idgt7EW>G*9%Q^K;o_#uFjX!V2pwfpgi>}J&p_^QlZki!@#dkvR`p?bckC`J*g z=%3PkFT3HAX2Q+dShHUbb1?ZcK8U7oaufLTCB#1W{=~k0Jabgv>q|H+GU=f-y|{p4 zwN|AE+YbCgx=7vlXE?@gkXW9PaqbO#GB=4$o0FkNT#EI?aLVd2(qnPK$Yh%YD%v(mdwn}bgsxyIBI^)tY?&G zi^2JfClZ@4b{xFjyTY?D61w@*ez2@5rWLpG#34id?>>oPg{`4F-l`7Lg@D@Hc}On} zx%BO4MsLYosLGACJ-d?ifZ35r^t*}wde>AAWO*J-X%jvD+gL9`u`r=kP zyeJ%FqqKfz8e_3K(M1RmB?gIYi{W7Z<THP2ihue0mbpu5n(x_l|e1tw(q!#m5lmef6ktqIb${ zV+ee#XRU}_dDDUiV@opHZ@EbQ<9qIZJMDsZDkW0^t3#j`S)G#>N^ZBs8k+FJhAfu< z%u!$%dyP3*_+jUvCf-%{x#MyDAK?#iPfE<(@Q0H7;a125eD%I(+!x1f;Sy`e<9>nm zQH4czZDQmW7^n>jL)@P@aAuAF$;I7JZE5a8~AJI5CNDqyf$gjloKR7C?OPt9yeH}n5 zNF8Vhmd%1O>T4EZD&0%Dt7YWNImmEV{7QF(dy!>q5k>Kh&Xy8hcBMUvVV~Xn8O&%{ z&q=JCYw#KlwM8%cu-rNadu(P~i3bM<_a{3!J*;vZhR6dln6#eW0^0kN)Vv3!bqM`w z{@j*eyzz=743dgFPY`Cx3|>ata;;_hQ3RJd+kU}~p~aphRx`03B>g4*~f%hUV+#D9rYRbsGD?jkB^$3XcgB|3N1L& zrmk9&Dg450mAd=Q_p?gIy5Zx7vRL?*rpNq76_rysFo)z)tp0B;7lSb9G5wX1vC9Lc z5Q8tb-alolVNWFsxO_=12o}X(>@Mwz1mkYh1##(qQwN=7VKz?61kay8A9(94Ky(4V zq6qd2+4a20Z0QRrmp6C?4;%U?@MatfXnkj&U6bP_&2Ny}BF%4{QhNx*Tabik9Y-~Z z@0WV6XD}aI(%pN}oW$X~Qo_R#+1$@J8(31?zM`#e`#(0f<-AZ^={^NgH#lc?oi(Mu zMk|#KR^Q;V@?&(sh5)D;-fu)rx%gXZ1&5)MR+Mhssy+W>V%S|PRNyTAd}74<(#J>H zR(1BfM%eIv0+ngHH6(i`?-%_4!6PpK*0X)79SX0X$`lv_q>9(E2kkkP;?c@rW2E^Q zs<;`9dg|lDMNECFrD3jTM^Mn-C$44}9d9Kc z#>*k&e#25;D^%82^1d@Yt{Y91MbEu0C}-;HR4+IaCeZ`l?)Q8M2~&E^FvJ?EBJJ(% zz1>tCW-E~FB}DI}z#+fUo+=kQME^=eH>^%V8w)dh*ugPFdhMUi3R2Cg}Zak4!k_8YW(JcR-)hY8C zXja}R7@%Q0&IzQTk@M|)2ViZDNCDRLNI)*lH%SDa^2TG4;%jE4n`8`aQAA$0SPH2@ z)2eWZuP26+uGq+m8F0fZn)X^|bNe z#f{qYZS!(CdBdM$N2(JH_a^b#R2=>yVf%JI_ieRFB{w&|o9txwMrVxv+n78*aXFGb z>Rkj2yq-ED<)A46T9CL^$iPynv`FoEhUM10@J+UZ@+*@_gyboQ>HY9CiwTUo7OM=w zd~$N)1@6U8H#Zu(wGLa_(Esx%h@*pmm5Y9OX@CY`3kPYPQx@z8yAgtm(+agDU%4?c zy8pR4SYbu8vY?JX6HgVq7|f=?w(%`m-C+a@E{euXo>XrGmkmFGzktI*rj*8D z)O|CHKXEzH{~iS+6)%ybRD|JRQ6j<+u_+=SgnJP%K+4$st+~XCVcAjI9e5`RYq$n{ zzy!X9Nv7>T4}}BZpSj9G9|(4ei-}Du<_IZw+CB`?fd$w^;=j8?vlp(#JOWiHaXJjB0Q00RHJ@sG6N#y^H7t^&V} z;VrDI4?75G$q5W9mV=J2iP24NHJy&d|HWHva>FaS#3AO?+ohh1__FMx;?`f{HG3v0 ztiO^Wanb>U4m9eLhoc_2B(ca@YdnHMB*~aYO+AE(&qh@?WukLbf_y z>*3?Xt-lxr?#}y%kTv+l8;!q?Hq8XSU+1E8x~o@9$)zO2z9K#(t`vPDri`mKhv|sh z{KREcy`#pnV>cTT7dm7M9B@9qJRt3lfo(C`CNkIq@>|2<(yn!AmVN?ST zbX_`JjtWa3&N*U{K7FYX8})*D#2@KBae` zhKS~s!r%SrXdhCsv~sF}7?ocyS?afya6%rDBu6g^b2j#TOGp^1zrMR}|70Z>CeYq- z1o|-=FBKlu{@;pm@QQJ_^!&hzi;0Z_Ho){x3O1KQ#TYk=rAt9`YKC0Y^}8GWIN{QW znYJyVTrmNvl!L=YS1G8BAxGmMUPi+Q7yb0XfG`l+L1NQVSbe^BICYrD;^(rke{jWCEZOtVv3xFze!=Z&(7}!)EcN;v0Dbit?RJ6bOr;N$ z=nk8}H<kCEE+IK3z<+3mkn4q!O7TMWpKShWWWM)X*)m6k%3luF6c>zOsFccvfLWf zH+mNkh!H@vR#~oe=ek}W3!71z$Dlj0c(%S|sJr>rvw!x;oCek+8f8s!U{DmfHcNpO z9>(IKOMfJwv?ey`V2ysSx2Npeh_x#bMh)Ngdj$al;5~R7Ac5R2?*f{hI|?{*$0qU- zY$6}ME%OGh^zA^z9zJUs-?a4ni8cw_{cYED*8x{bWg!Fn9)n;E9@B+t;#k}-2_j@# zg#b%R(5_SJAOtfgFCBZc`n<&z6)%nOIu@*yo!a% zpLg#36KBN$01W{b;qWN`Tp(T#jh%;Zp_zpS64lvBVY2B#UK)p`B4Oo)IO3Z&D6<3S zfF?ZdeNEnzE{}#gyuv)>;z6V{!#bx)` zY;hL*f(WVD*D9A4$WbRKF2vf;MoZVdhfWbWhr{+Db5@M^A4wrFReuWWimA4qp`GgoL2`W4WPUL5A=y3Y3P z%G?8lLUhqo@wJW8VDT`j&%YY7xh51NpVYlsrk_i4J|pLO(}(b8_>%U2M`$iVRDc-n zQiOdJbroQ%*vhN{!{pL~N|cfGooK_jTJCA3g_qs4c#6a&_{&$OoSQr_+-O^mKP=Fu zGObEx`7Qyu{nHTGNj(XSX*NPtAILL(0%8Jh)dQh+rtra({;{W2=f4W?Qr3qHi*G6B zOEj7%nw^sPy^@05$lOCjAI)?%B%&#cZ~nC|=g1r!9W@C8T0iUc%T*ne z)&u$n>Ue3FN|hv+VtA+WW)odO-sdtDcHfJ7s&|YCPfWaVHpTGN46V7Lx@feE#Od%0XwiZy40plD%{xl+K04*se zw@X4&*si2Z_0+FU&1AstR)7!Th(fdaOlsWh`d!y=+3m!QC$Zlkg8gnz!}_B7`+wSz z&kD?6{zPnE3uo~Tv8mLP%RaNt2hcCJBq=0T>%MW~Q@Tpt2pPP1?KcywH>in5@ zx+5;xu-ltFfo5vLU;2>r$-KCHjwGR&1XZ0YNyrXXAUK!FLM_7mV&^;;X^*YH(FLRr z`0Jjg7wiq2bisa`CG%o9i)o1`uG?oFjU_Zrv1S^ipz$G-lc^X@~6*)#%nn+RbgksJfl{w=k31(q>7a!PCMp5YY{+Neh~mo zG-3dd!0cy`F!nWR?=9f_KP$X?Lz&cLGm_ohy-|u!VhS1HG~e7~xKpYOh=GmiiU;nu zrZ5tWfan3kp-q_vO)}vY6a$19Q6UL0r znJ+iSHN-&w@vDEZ0V%~?(XBr|jz&vrBNLOngULxtH(Rp&U*rMY42n;05F11xh?k;n_DX2$4|vWIkXnbwfC z=ReH=(O~a;VEgVO?>qsP*#eOC9Y<_9Yt<6X}X{PyF7UXIA$f)>NR5P&4G_Ygq(9TwwQH*P>Rq>3T4I+t2X(b5ogXBAfNf!xiF#Gilm zp2h{&D4k!SkKz-SBa%F-ZoVN$7GX2o=(>vkE^j)BDSGXw?^%RS9F)d_4}PN+6MlI8*Uk7a28CZ)Gp*EK)`n5i z){aq=0SFSO-;sw$nAvJU-$S-cW?RSc7kjEBvWDr1zxb1J7i;!i+3PQwb=)www?7TZ zE~~u)vO>#55eLZW;)F(f0KFf8@$p)~llV{nO7K_Nq-+S^h%QV_CnXLi)p*Pq&`s!d zK2msiR;Hk_rO8`kqe_jfTmmv|$MMo0ll}mI)PO4!ikVd(ZThhi&4ZwK?tD-}noj}v zBJ?jH-%VS|=t)HuTk?J1XaDUjd_5p1kPZi6y#F6$lLeRQbj4hsr=hX z4tXkX2d5DeLMcAYTeYm|u(XvG5JpW}hcOs4#s8g#ihK%@hVz|kL=nfiBqJ{*E*WhC zht3mi$P3a(O5JiDq$Syu9p^HY&9~<#H89D8 zJm84@%TaL_BZ+qy8+T3_pG7Q%z80hnjN;j>S=&WZWF48PDD%55lVuC0%#r5(+S;WH zS7!HEzmn~)Ih`gE`faPRjPe^t%g=F ztpGVW=Cj5ZkpghCf~`ar0+j@A=?3(j@7*pq?|9)n*B4EQTA1xj<+|(Y72?m7F%&&& zdO44owDBPT(8~RO=dT-K4#Ja@^4_0v$O3kn73p6$s?mCmVDUZ+Xl@QcpR6R3B$=am z%>`r9r2Z79Q#RNK?>~lwk^nQlR=Hr-ji$Ss3ltbmB)x@0{VzHL-rxVO(++@Yr@Iu2 zTEX)_9sVM>cX$|xuqz~Y8F-(n;KLAfi*63M7mh&gsPR>N0pd9h!0bm%nA?Lr zS#iEmG|wQd^BSDMk0k?G>S-uE$vtKEF8Dq}%vLD07zK4RLoS?%F1^oZZI$0W->7Z# z?v&|a`u#UD=_>i~`kzBGaPj!mYX5g?3RC4$5EV*j0sV)>H#+$G6!ci=6`)85LWR=FCp-NUff`;2zG9nU6F~ z;3ZyE*>*LvUgae+uMf}aV}V*?DCM>{o31+Sx~6+sz;TI(VmIpDrN3z+BUj`oGGgLP z>h9~MP}Pw#YwzfGP8wSkz`V#}--6}7S9yZvb{;SX?6PM_KuYpbi~*=teZr-ga2QqIz{QrEyZ@>eN*qmy;N@FCBbRNEeeoTmQyrX;+ zCkaJ&vOIbc^2BD6_H+Mrcl?Nt7O{xz9R_L0ZPV_u!sz+TKbXmhK)0QWoe-_HwtKJ@@7=L+ z+K8hhf=4vbdg3GqGN<;v-SMIzvX=Z`WUa_91Yf89^#`G(f-Eq>odB^p-Eqx}ENk#&MxJ+%~Ad2-*`1LNT>2INPw?*V3&kE;tt?rQyBw? zI+xJD04GTz1$7~KMnfpkPRW>f%n|0YCML@ODe`10;^DXX-|Hb*IE%_Vi#Pn9@#ufA z_8NY*1U%VseqYrSm?%>F@`laz+f?+2cIE4Jg6 z_VTcx|DSEA`g!R%RS$2dSRM|9VQClsW-G<~=j5T`pTbu-x6O`R z98b;}`rPM(2={YiytrqX+uh65f?%XiPp`;4CcMT*E*dQJ+if9^D>c_Dk8A(cE<#r=&!& z_`Z01=&MEE+2@yr!|#El=yM}v>i=?w^2E_FLPy(*4A9XmCNy>cBWdx3U>1RylsItO z4V8T$z3W-qqq*H`@}lYpfh=>C!tieKhoMGUi)EpWDr;yIL&fy};Y&l|)f^QE*k~4C zH>y`Iu%#S)z)YUqWO%el*Z)ME#p{1_8-^~6UF;kBTW zMQ!eXQuzkR#}j{qb(y9^Y!X7&T}}-4$%4w@w=;w+>Z%uifR9OoQ>P?0d9xpcwa>7kTv2U zT-F?3`Q`7xOR!gS@j>7In>_h){j#@@(ynYh;nB~}+N6qO(JO1xA z@59Pxc#&I~I64slNR?#hB-4XE>EFU@lUB*D)tu%uEa))B#eJ@ZOX0hIulfnDQz-y8 z`CX@(O%_VC{Ogh&ot``jlDL%R!f>-8yq~oLGxBO?+tQb5%k@a9zTs!+=NOwSVH-cR zqFo^jHeXDA_!rx$NzdP;>{-j5w3QUrR<;}=u2|FBJ;D#v{SK@Z6mjeV7_kFmWt95$ zeGaF{IU?U>?W`jzrG_9=9}yN*LKyzz))PLE+)_jc#4Rd$yFGol;NIk(qO1$5VXR)+ zxF7%f4=Q!NzR>DVXUB&nUT&>Nyf+5QRF+Z`X-bB*7=`|Go5D1&h~ zflKLw??kpiRm0h3|1GvySC2^#kcFz^5{79KKlq@`(leBa=_4CgV9sSHr{RIJ^KwR_ zY??M}-x^=MD+9`v@I3jue=OCn0kxno#6i>b(XKk_XTp_LpI}X*UA<#* zsgvq@yKTe_dTh>q1aeae@8yur08S(Q^8kXkP_ty48V$pX#y9)FQa~E7P7}GP_CbCm zc2dQxTeW(-~Y6}im24*XOC8ySfH*HMEnW3 z4CXp8iK(Nk<^D$g0kUW`8PXn2kdcDk-H@P0?G8?|YVlIFb?a>QunCx%B9TzsqQQ~HD!UO7zq^V!v9jho_FUob&Hxi ztU1nNOK)a!gkb-K4V^QVX05*>-^i|{b`hhvQLyj`E1vAnj0fbqqO%r z6Q;X1x0dL~GqMv%8QindZ4CZ%7pYQW~ z9)I*#Gjref-q(4Z*E#1c&rE0-_(4;_M(V7rgH_7H;ps1s%GBmU z{4a|X##j#XUF2n({v?ZUUAP5k>+)^F)7n-npbV3jAlY8V3*W=fwroDS$c&r$>8aH` zH+irV{RG3^F3oW2&E%5hXgMH9>$WlqX76Cm+iFmFC-DToTa`AcuN9S!SB+BT-IA#3P)JW1m~Cuwjs`Ep(wDXE4oYmt*aU z!Naz^lM}B)JFp7ejro7MU9#cI>wUoi{lylR2~s)3M!6a=_W~ITXCPd@U9W)qA5(mdOf zd3PntGPJyRX<9cgX?(9~TZB5FdEHW~gkJXY51}?s4ZT_VEdwOwD{T2E-B>oC8|_ZwsPNj=-q(-kwy%xX2K0~H z{*+W`-)V`7@c#Iuaef=?RR2O&x>W0A^xSwh5MsjTz(DVG-EoD@asu<>72A_h<39_# zawWVU<9t{r*e^u-5Q#SUI6dV#p$NYEGyiowT>>d*or=Ps!H$-3={bB|An$GPkP5F1 zTnu=ktmF|6E*>ZQvk^~DX(k!N`tiLut*?3FZhs$NUEa4ccDw66-~P;x+0b|<!ZN7Z%A`>2tN#CdoG>((QR~IV_Gj^Yh%!HdA~4C3jOXaqb6Ou z21T~Wmi9F6(_K0@KR@JDTh3-4mv2=T7&ML<+$4;b9SAtv*Uu`0>;VVZHB{4?aIl3J zL(rMfk?1V@l)fy{J5DhVlj&cWKJCcrpOAad(7mC6#%|Sn$VwMjtx6RDx1zbQ|Ngg8N&B56DGhu;dYg$Z{=YmCNn+?ceDclp65c_RnKs4*vefnhudSlrCy6-96vSB4_sFAj# zftzECwmNEOtED^NUt{ZDjT7^g>k1w<=af>+0)%NA;IPq6qx&ya7+QAu=pk8t>KTm` zEBj9J*2t|-(h)xc>Us*jHs)w9qmA>8@u21UqzKk*Ei#0kCeW6o z-2Q+Tvt25IUkb}-_LgD1_FUJ!U8@8OC^9(~Kd*0#zr*8IQkD)6Keb(XFai5*DYf~` z@U?-{)9X&BTf!^&@^rjmvea#9OE~m(D>qfM?CFT9Q4RxqhO0sA7S)=--^*Q=kNh7Y zq%2mu_d_#23d`+v`Ol263CZ<;D%D8Njj6L4T`S*^{!lPL@pXSm>2;~Da- zBX97TS{}exvSva@J5FJVCM$j4WDQuME`vTw>PWS0!;J7R+Kq zVUy6%#n5f7EV(}J#FhDpts;>=d6ow!yhJj8j>MJ@Wr_?x30buuutIG97L1A*QFT$c ziC5rBS;#qj=~yP-yWm-p(?llTwDuhS^f&<(9vA9@UhMH2-Fe_YAG$NvK6X{!mvPK~ zuEA&PA}meylmaIbbJXDOzuIn8cJNCV{tUA<$Vb?57JyAM`*GpEfMmFq>)6$E(9e1@W`l|R%-&}38#bl~levA#fx2wiBk^)mPj?<=S&|gv zQO)4*91$n08@W%2b|QxEiO0KxABAZC{^4BX^6r>Jm?{!`ZId9jjz<%pl(G5l));*`UU3KfnuXSDj2aP>{ zRIB$9pm7lj3*Xg)c1eG!cb+XGt&#?7yJ@C)(Ik)^OZ5><4u$VLCqZ#q2NMCt5 z6$|VN(RWM;5!JV?-h<JkEZ(SZF zC(6J+>A6Am9H7OlOFq6S62-2&z^Np=#xXsOq0WUKr zY_+Ob|CQd1*!Hirj5rn*=_bM5_zKmq6lG zn*&_=x%?ATxZ8ZTzd%biKY_qyNC#ZQ1vX+vc48N>aJXEjs{Y*3Op`Q7-oz8jyAh>d zNt_qvn`>q9aO~7xm{z`ree%lJ3YHCyC`q`-jUVCn*&NIml!uuMNm|~u3#AV?6kC+B z?qrT?xu2^mobSlzb&m(8jttB^je0mx;TT8}`_w(F11IKz83NLj@OmYDpCU^u?fD{) z&=$ptwVw#uohPb2_PrFX;X^I=MVXPDpqTuYhRa>f-=wy$y3)40-;#EUDYB1~V9t%$ z^^<7Zbs0{eB93Pcy)96%XsAi2^k`Gmnypd-&x4v9rAq<>a(pG|J#+Q>E$FvMLmy7T z5_06W=*ASUyPRfgCeiPIe{b47Hjqpb`9Xyl@$6*ntH@SV^bgH&Fk3L9L=6VQb)Uqa z33u#>ecDo&bK(h1WqSH)b_Th#Tvk&%$NXC@_pg5f-Ma#7q;&0QgtsFO~`V&{1b zbSP*X)jgLtd@9XdZ#2_BX4{X~pS8okF7c1xUhEV9>PZco>W-qz7YMD`+kCGULdK|^ zE7VwQ-at{%&fv`a+b&h`TjzxsyQX05UB~a0cuU-}{*%jR48J+yGWyl3Kdz5}U>;lE zgkba*yI5>xqIPz*Y!-P$#_mhHB!0Fpnv{$k-$xxjLAc`XdmHd1k$V@2QlblfJPrly z*~-4HVCq+?9vha>&I6aRGyq2VUon^L1a)g`-Xm*@bl2|hi2b|UmVYW|b+Gy?!aS-p z86a}Jep6Mf>>}n^*Oca@Xz}kxh)Y&pX$^CFAmi#$YVf57X^}uQD!IQSN&int=D> zJ>_|au3Be?hmPKK)1^JQ(O29eTf`>-x^jF2xYK6j_9d_qFkWHIan5=7EmDvZoQWz5 zZGb<{szHc9Nf@om)K_<=FuLR<&?5RKo3LONFQZ@?dyjemAe4$yDrnD zglU#XYo6|~L+YpF#?deK6S{8A*Ou;9G`cdC4S0U74EW18bc5~4>)<*}?Z!1Y)j;Ot zosEP!pc$O^wud(={WG%hY07IE^SwS-fGbvpP?;l8>H$;}urY2JF$u#$q}E*ZG%fR# z`p{xslcvG)kBS~B*^z6zVT@e}imYcz_8PRzM4GS52#ms5Jg9z~ME+uke`(Tq1w3_6 zxUa{HerS7!Wq&y(<9yyN@P^PrQT+6ij_qW3^Q)I53iIFCJE?MVyGLID!f?QHUi1tq z0)RNIMGO$2>S%3MlBc09l!6_(ECxXTU>$KjWdZX^3R~@3!SB zah5Za2$63;#y!Y}(wg1#shMePQTzfQfXyJ-Tf`R05KYcyvo8UW9-IWGWnzxR6Vj8_la;*-z5vWuwUe7@sKr#Tr51d z2PWn5h@|?QU3>k=s{pZ9+(}oye zc*95N_iLmtmu}H-t$smi49Y&ovX}@mKYt2*?C-i3Lh4*#q5YDg1Mh`j9ovRDf9&& zp_UMQh`|pC!|=}1uWoMK5RAjdTg3pXPCsYmRkWW}^m&)u-*c_st~gcss(`haA)xVw zAf=;s>$`Gq_`A}^MjY_BnCjktBNHY1*gzh(i0BFZ{Vg^F?Pbf`8_clvdZ)5(J4EWzAP}Ba5zX=S(2{gDugTQ3`%!q`h7kYSnwC`zEWeuFlODKiityMaM9u{Z%E@@y1jmZA#ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 46993 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 diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index ed4cc16421680a50164ba74381b4b35ceaa0ccfc..13b35eba55c6dabc3aac36f33d859266c18fa0d0 100644 GIT binary patch literal 5680 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 literal 3276 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 N;OXk;vd$@?2>?>Ex^Vyi diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bcbf36df2f2aaaa0a63c7dabc94e600184229d0d..bdb57226d5f2bd20f11934f4903f16459cf52379 100644 GIT binary patch literal 14142 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 diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index e71a726136a47ed24125c7efc79d68a4a01961b4..326c0e72c9d820600887813b3b98d0dd69c5d4e8 100644 GIT binary patch literal 36406 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 literal 14800 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 vNcLuCM3Rj?iBw@67X_p?_CF#jIyB-s + + + + diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig index f960fd48..41f79de2 100644 --- a/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/example/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = example +PRODUCT_NAME = flutter_tts_example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.tundralabs.example +PRODUCT_BUNDLE_IDENTIFIER = com.tundralabs.fluttertts.example.flutterTtsExample // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2020 com.tundralabs. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2025 com.tundralabs.fluttertts.example. All rights reserved. diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift index 2722837e..3cc05eb2 100644 --- a/example/macos/Runner/MainFlutterWindow.swift +++ b/example/macos/Runner/MainFlutterWindow.swift @@ -3,7 +3,7 @@ import FlutterMacOS class MainFlutterWindow: NSWindow { override func awakeFromNib() { - let flutterViewController = FlutterViewController.init() + let flutterViewController = FlutterViewController() let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) diff --git a/example/macos/RunnerTests/RunnerTests.swift b/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 1f16ca43..c77704cf 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,5 +1,11 @@ name: flutter_tts_example description: Demonstrates how to use the flutter_tts plugin. +version: 0.0.1+1 +publish_to: none + +environment: + sdk: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" dependencies: flutter: @@ -8,22 +14,19 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.1+1 + flutter_tts: + path: ../ dev_dependencies: flutter_test: sdk: flutter + flutter_lints: ^5.0.0 - flutter_tts: - path: ../ - -dependency_overrides: - material_color_utilities: 0.11.1 # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec # The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. @@ -57,8 +60,5 @@ flutter: # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # - # For details regarding fonts from package dependencies, + # For details regarding fonts from package dependencies, # see https://flutter.io/custom-fonts/#from-packages - -environment: - sdk: '>=2.12.0-0 <3.0.0' diff --git a/ios/Classes/AudioCategory.swift b/ios/Classes/AudioCategory.swift index 74e8ea0b..367c2662 100644 --- a/ios/Classes/AudioCategory.swift +++ b/ios/Classes/AudioCategory.swift @@ -1,21 +1,16 @@ import AVFoundation -enum AudioCategory: String { - case iosAudioCategoryAmbientSolo - case iosAudioCategoryAmbient - case iosAudioCategoryPlayback - case iosAudioCategoryPlaybackAndRecord - - func toAVAudioSessionCategory() -> AVAudioSession.Category { - switch self { - case .iosAudioCategoryAmbientSolo: - return .soloAmbient - case .iosAudioCategoryAmbient: - return .ambient - case .iosAudioCategoryPlayback: - return .playback - case .iosAudioCategoryPlaybackAndRecord: - return .playAndRecord +extension IosTextToSpeechAudioCategory { + func toAVAudioSessionCategory() -> AVAudioSession.Category { + switch self { + case IosTextToSpeechAudioCategory.ambientSolo: + return .soloAmbient + case IosTextToSpeechAudioCategory.ambient: + return .ambient + case IosTextToSpeechAudioCategory.playback: + return .playback + case IosTextToSpeechAudioCategory.playAndRecord: + return .playAndRecord + } } - } } diff --git a/ios/Classes/AudioCategoryOptions.swift b/ios/Classes/AudioCategoryOptions.swift index f593bfdb..fa3e7e3e 100644 --- a/ios/Classes/AudioCategoryOptions.swift +++ b/ios/Classes/AudioCategoryOptions.swift @@ -1,42 +1,30 @@ import AVFoundation -enum AudioCategoryOptions: String { - case iosAudioCategoryOptionsMixWithOthers - case iosAudioCategoryOptionsDuckOthers - case iosAudioCategoryOptionsInterruptSpokenAudioAndMixWithOthers - case iosAudioCategoryOptionsAllowBluetooth - case iosAudioCategoryOptionsAllowBluetoothA2DP - case iosAudioCategoryOptionsAllowAirPlay - case iosAudioCategoryOptionsDefaultToSpeaker - - func toAVAudioSessionCategoryOptions() -> AVAudioSession.CategoryOptions? { - switch self { - case .iosAudioCategoryOptionsMixWithOthers: - return .mixWithOthers - case .iosAudioCategoryOptionsDuckOthers: - return .duckOthers - case .iosAudioCategoryOptionsInterruptSpokenAudioAndMixWithOthers: - if #available(iOS 9.0, *) { - return .interruptSpokenAudioAndMixWithOthers - } else { - return nil - } - case .iosAudioCategoryOptionsAllowBluetooth: - return .allowBluetooth - case .iosAudioCategoryOptionsAllowBluetoothA2DP: - if #available(iOS 10.0, *) { - return .allowBluetoothA2DP - } else { - return nil - } - case .iosAudioCategoryOptionsAllowAirPlay: - if #available(iOS 10.0, *) { - return .allowAirPlay - } else { - return nil - } - case .iosAudioCategoryOptionsDefaultToSpeaker: - return .defaultToSpeaker +extension IosTextToSpeechAudioCategoryOptions { + func toAVAudioSessionCategoryOptions() -> AVAudioSession.CategoryOptions? { + switch self { + case IosTextToSpeechAudioCategoryOptions.mixWithOthers: + return .mixWithOthers + case IosTextToSpeechAudioCategoryOptions.duckOthers: + return .duckOthers + case IosTextToSpeechAudioCategoryOptions.interruptSpokenAudioAndMixWithOthers: + if #available(iOS 9.0, *) { + return .interruptSpokenAudioAndMixWithOthers + } else { + return nil + } + case IosTextToSpeechAudioCategoryOptions.allowBluetooth: + return .allowBluetoothHFP + case IosTextToSpeechAudioCategoryOptions.allowBluetoothA2DP: + return .allowBluetoothA2DP + case IosTextToSpeechAudioCategoryOptions.allowAirPlay: + if #available(iOS 10.0, *) { + return .allowBluetoothA2DP + } else { + return nil + } + case IosTextToSpeechAudioCategoryOptions.defaultToSpeaker: + return .defaultToSpeaker + } } - } } diff --git a/ios/Classes/AudioModes.swift b/ios/Classes/AudioModes.swift index e2e30716..721f64fd 100644 --- a/ios/Classes/AudioModes.swift +++ b/ios/Classes/AudioModes.swift @@ -1,63 +1,53 @@ import AVFoundation -enum AudioModes: String { - case iosAudioModeDefault - case iosAudioModeGameChat - case iosAudioModeMeasurement - case iosAudioModeMoviePlayback - case iosAudioModeSpokenAudio - case iosAudioModeVideoChat - case iosAudioModeVideoRecording - case iosAudioModeVoiceChat - case iosAudioModeVoicePrompt - - func toAVAudioSessionMode() -> AVAudioSession.Mode? { - switch self { - case .iosAudioModeDefault: - if #available(iOS 12.0, *) { - return .default - } - return nil - case .iosAudioModeGameChat: - if #available(iOS 12.0, *) { - return .gameChat - } - return nil - case .iosAudioModeMeasurement: - if #available(iOS 12.0, *) { - return .measurement - } - return nil - case .iosAudioModeMoviePlayback: - if #available(iOS 12.0, *) { - return .moviePlayback - } - return nil - case .iosAudioModeSpokenAudio: - if #available(iOS 12.0, *) { - return .spokenAudio - } - return nil - case .iosAudioModeVideoChat: - if #available(iOS 12.0, *) { - return .videoChat - } - return nil - case .iosAudioModeVideoRecording: - if #available(iOS 12.0, *) { - return .videoRecording - } - return nil - case .iosAudioModeVoiceChat: - if #available(iOS 12.0, *) { - return .voiceChat - } - return nil - case .iosAudioModeVoicePrompt: - if #available(iOS 12.0, *) { - return .voicePrompt +extension IosTextToSpeechAudioMode { + func toAVAudioSessionMode() -> AVAudioSession.Mode? { + switch self { + case IosTextToSpeechAudioMode.defaultMode: + if #available(iOS 12.0, *) { + return .default + } + return nil + case IosTextToSpeechAudioMode.gameChat: + if #available(iOS 12.0, *) { + return .gameChat + } + return nil + case IosTextToSpeechAudioMode.measurement: + if #available(iOS 12.0, *) { + return .measurement + } + return nil + case IosTextToSpeechAudioMode.moviePlayback: + if #available(iOS 12.0, *) { + return .moviePlayback + } + return nil + case IosTextToSpeechAudioMode.spokenAudio: + if #available(iOS 12.0, *) { + return .spokenAudio + } + return nil + case IosTextToSpeechAudioMode.videoChat: + if #available(iOS 12.0, *) { + return .videoChat + } + return nil + case IosTextToSpeechAudioMode.videoRecording: + if #available(iOS 12.0, *) { + return .videoRecording + } + return nil + case IosTextToSpeechAudioMode.voiceChat: + if #available(iOS 12.0, *) { + return .voiceChat + } + return nil + case IosTextToSpeechAudioMode.voicePrompt: + if #available(iOS 12.0, *) { + return .voicePrompt + } + return nil } - return nil } - } } diff --git a/ios/Classes/FlutterTtsPlugin.h b/ios/Classes/FlutterTtsPlugin.h deleted file mode 100644 index 13e3a21a..00000000 --- a/ios/Classes/FlutterTtsPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface FlutterTtsPlugin : NSObject -@end diff --git a/ios/Classes/FlutterTtsPlugin.m b/ios/Classes/FlutterTtsPlugin.m deleted file mode 100644 index 3fc014ff..00000000 --- a/ios/Classes/FlutterTtsPlugin.m +++ /dev/null @@ -1,15 +0,0 @@ -#import "FlutterTtsPlugin.h" -#if __has_include() -#import -#else -// Support project import fallback if the generated compatibility header -// is not copied when this plugin is created as a library. -// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 -#import "flutter_tts-Swift.h" -#endif - -@implementation FlutterTtsPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftFlutterTtsPlugin registerWithRegistrar:registrar]; -} -@end diff --git a/ios/Classes/SwiftFlutterTtsPlugin.swift b/ios/Classes/SwiftFlutterTtsPlugin.swift index 7b18a353..98c7ad98 100644 --- a/ios/Classes/SwiftFlutterTtsPlugin.swift +++ b/ios/Classes/SwiftFlutterTtsPlugin.swift @@ -1,481 +1,522 @@ +import AVFoundation import Flutter import UIKit -import AVFoundation -public class SwiftFlutterTtsPlugin: NSObject, FlutterPlugin, AVSpeechSynthesizerDelegate { - let iosAudioCategoryKey = "iosAudioCategoryKey" - let iosAudioCategoryOptionsKey = "iosAudioCategoryOptionsKey" - let iosAudioModeKey = "iosAudioModeKey" - - let synthesizer = AVSpeechSynthesizer() - var rate: Float = AVSpeechUtteranceDefaultSpeechRate - var volume: Float = 1.0 - var pitch: Float = 1.0 - var voice: AVSpeechSynthesisVoice? - var awaitSpeakCompletion: Bool = false - var awaitSynthCompletion: Bool = false - var autoStopSharedSession: Bool = true - var speakResult: FlutterResult? = nil - var synthResult: FlutterResult? = nil - - lazy var audioSession = AVAudioSession.sharedInstance() - lazy var language: String = { - AVSpeechSynthesisVoice.currentLanguageCode() - }() - lazy var languages: Set = { - Set(AVSpeechSynthesisVoice.speechVoices().map(\.language)) - }() - - - var channel = FlutterMethodChannel() - init(channel: FlutterMethodChannel) { - super.init() - self.channel = channel - synthesizer.delegate = self - } - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "flutter_tts", binaryMessenger: registrar.messenger()) - let instance = SwiftFlutterTtsPlugin(channel: channel) - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "speak": - let text: String = call.arguments as! String - self.speak(text: text, result: result) - break - case "awaitSpeakCompletion": - self.awaitSpeakCompletion = call.arguments as! Bool - result(1) - break - case "awaitSynthCompletion": - self.awaitSynthCompletion = call.arguments as! Bool - result(1) - break - case "synthesizeToFile": - guard let args = call.arguments as? [String: Any] else { - result("iOS could not recognize flutter arguments in method: (sendParams)") - return - } - let text = args["text"] as! String - let fileName = args["fileName"] as! String - let isFullPath = args["isFullPath"] as! Bool - self.synthesizeToFile(text: text, fileName: fileName, isFullPath: isFullPath, result: result) - break - case "pause": - self.pause(result: result) - break - case "setLanguage": - let language: String = call.arguments as! String - self.setLanguage(language: language, result: result) - break - case "setSpeechRate": - let rate: Double = call.arguments as! Double - self.setRate(rate: Float(rate)) - result(1) - break - case "setVolume": - let volume: Double = call.arguments as! Double - self.setVolume(volume: Float(volume), result: result) - break - case "setPitch": - let pitch: Double = call.arguments as! Double - self.setPitch(pitch: Float(pitch), result: result) - break - case "stop": - self.stop() - result(1) - break - case "getLanguages": - self.getLanguages(result: result) - break - case "getSpeechRateValidRange": - self.getSpeechRateValidRange(result: result) - break - case "isLanguageAvailable": - let language: String = call.arguments as! String - self.isLanguageAvailable(language: language, result: result) - break - case "getVoices": - self.getVoices(result: result) - break - case "setVoice": - guard let args = call.arguments as? [String: String] else { - result("iOS could not recognize flutter arguments in method: (sendParams)") - return - } - self.setVoice(voice: args, result: result) - break - case "clearVoice": - self.clearVoice() - result(1) - break - case "setSharedInstance": - let sharedInstance = call.arguments as! Bool - self.setSharedInstance(sharedInstance: sharedInstance, result: result) - break - case "autoStopSharedSession": - let autoStop = call.arguments as! Bool - self.autoStopSharedSession = autoStop - result(1) - break - case "setIosAudioCategory": - guard let args = call.arguments as? [String: Any] else { - result("iOS could not recognize flutter arguments in method: (sendParams)") - return - } - let audioCategory = args["iosAudioCategoryKey"] as? String - let audioOptions = args[iosAudioCategoryOptionsKey] as? Array - let audioModes = args[iosAudioModeKey] as? String - self.setAudioCategory(audioCategory: audioCategory, audioOptions: audioOptions, audioMode: audioModes, result: result) - break - default: - result(FlutterMethodNotImplemented) - } - } - - private func speak(text: String, result: @escaping FlutterResult) { - if (self.synthesizer.isPaused) { - if (self.synthesizer.continueSpeaking()) { - if self.awaitSpeakCompletion { - self.speakResult = result +extension FlutterTtsErrorCode { + func toStrCode() -> String { + return "FlutterTtsErrorCode.\(rawValue)" + } +} + +let kVoiceSelectionNotSuported = "Voice selection is not supported below iOS 9.0" + +/// 带泛型结果类型 R 的 completion 别名 +typealias ResultCallback = (Result) -> Void + +public class FlutterTtsPlugin: NSObject, FlutterPlugin, AVSpeechSynthesizerDelegate, TtsHostApi, + IosTtsHostApi +{ + let iosAudioCategoryKey = "iosAudioCategoryKey" + let iosAudioCategoryOptionsKey = "iosAudioCategoryOptionsKey" + let iosAudioModeKey = "iosAudioModeKey" + + let synthesizer = AVSpeechSynthesizer() + var rate: Float = AVSpeechUtteranceDefaultSpeechRate + var volume: Float = 1.0 + var pitch: Float = 1.0 + var voice: AVSpeechSynthesisVoice? + var awaitSpeakCompletion: Bool = false + var awaitSynthCompletion: Bool = false + var autoStopSharedSessionImpl: Bool = true + var speakResult: ResultCallback? + var synthResult: ResultCallback? + + lazy var audioSession = AVAudioSession.sharedInstance() + lazy var language: String = AVSpeechSynthesisVoice.currentLanguageCode() + + lazy var languages: Set = Set(AVSpeechSynthesisVoice.speechVoices().map(\.language)) + + var flutterApi: TtsFlutterApi + init(flutterApi: TtsFlutterApi) { + self.flutterApi = flutterApi + super.init() + synthesizer.delegate = self + } + + public static func register(with registrar: FlutterPluginRegistrar) { + let flutterApi = TtsFlutterApi(binaryMessenger: registrar.messenger()) + let instance = FlutterTtsPlugin(flutterApi: flutterApi) + TtsHostApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance) + IosTtsHostApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance) + } + + func speak(text: String, forceFocus: Bool, completion: @escaping (Result) -> Void) { + speakImpl(text: text, completion: completion) + } + + func pause(completion: @escaping (Result) -> Void) { + pauseImpl(completion: completion) + } + + func stop(completion: @escaping (Result) -> Void) { + stopImpl() + completion(Result.success(TtsResult(success: true))) + } + + func setSpeechRate(rate: Double, completion: @escaping (Result) -> Void) { + setRateImpl(rate: Float(rate)) + completion(Result.success(TtsResult(success: true))) + } + + func setVolume(volume: Double, completion: @escaping (Result) -> Void) { + setVolumeImpl(volume: Float(volume), completion: completion) + } + + func setPitch(pitch: Double, completion: @escaping (Result) -> Void) { + setPitchImpl(pitch: Float(pitch), completion: completion) + } + + func setVoice(voice: Voice, completion: @escaping (Result) -> Void) { + setVoiceImpl(voice: voice, completion: completion) + } + + func clearVoice(completion: @escaping (Result) -> Void) { + clearVoiceImpl() + completion(Result.success(TtsResult(success: true))) + } + + func awaitSpeakCompletion(awaitCompletion: Bool, completion: @escaping (Result) -> Void) { + awaitSpeakCompletion = awaitCompletion + completion(Result.success(TtsResult(success: true))) + } + + func getLanguages(completion: @escaping (Result<[String], any Error>) -> Void) { + getLanguagesImpl(completion: completion) + } + + func getVoices(completion: @escaping (Result<[Voice], any Error>) -> Void) { + getVoicesImpl(completion: completion) + } + + func awaitSynthCompletion(awaitCompletion: Bool, completion: @escaping (Result) -> Void) { + awaitSynthCompletion = awaitCompletion + completion(Result.success(TtsResult(success: true))) + } + + func synthesizeToFile( + text: String, + fileName: String, + isFullPath: Bool, + completion: @escaping (Result) -> Void + ) { + synthesizeToFileImpl(text: text, fileName: fileName, isFullPath: isFullPath, completion: completion) + } + + func setSharedInstance(sharedSession: Bool, completion: @escaping (Result) -> Void) { + setSharedInstanceImpl(sharedInstance: sharedSession, completion: completion) + } + + func autoStopSharedSession(autoStop: Bool, completion: @escaping (Result) -> Void) { + autoStopSharedSessionImpl = autoStop + completion(Result.success(TtsResult(success: true))) + } + + func setIosAudioCategory( + category: IosTextToSpeechAudioCategory, + options: [IosTextToSpeechAudioCategoryOptions], + mode: IosTextToSpeechAudioMode, + completion: @escaping (Result) -> Void + ) { + setAudioCategoryImpl(category: category, audioOptions: options, mode: mode, completion: completion) + } + + func getSpeechRateValidRange(completion: @escaping (Result) -> Void) { + getSpeechRateValidRangeImpl(completion: completion) + } + + func setLanguange(language: String, completion: @escaping (Result) -> Void) { + setLanguageImpl(language: language, completion: completion) + } + + func isLanguageAvailable(language: String, completion: @escaping (Result) -> Void) { + isLanguageAvailableImpl(language: language, completion: completion) + } + + private func speakImpl(text: String, completion: @escaping ResultCallback) { + if synthesizer.isPaused { + if synthesizer.continueSpeaking() { + if awaitSpeakCompletion { + speakResult = completion + } else { + completion(Result.success(TtsResult(success: true))) + } + } else { + completion(Result.success(TtsResult(success: false))) + } + } else { + let utterance = AVSpeechUtterance(string: text) + if voice != nil { + utterance.voice = voice! + } else { + utterance.voice = AVSpeechSynthesisVoice(language: language) + } + utterance.rate = rate + utterance.volume = volume + utterance.pitchMultiplier = pitch + + synthesizer.speak(utterance) + if awaitSpeakCompletion { + speakResult = completion + } else { + completion(Result.success(TtsResult(success: true))) + } + } + } + + private func synthesizeToFileImpl( + text: String, fileName: String, isFullPath: Bool, + completion: @escaping ResultCallback + ) { + var output: AVAudioFile? + var failed = false + let utterance = AVSpeechUtterance(string: text) + + if voice != nil { + utterance.voice = voice! + } else { + utterance.voice = AVSpeechSynthesisVoice(language: language) + } + utterance.rate = rate + utterance.volume = volume + utterance.pitchMultiplier = pitch + + if #available(iOS 13.0, *) { + self.synthesizer.write(utterance) { (buffer: AVAudioBuffer) in + guard let pcmBuffer = buffer as? AVAudioPCMBuffer else { + NSLog("unknow buffer type: \(buffer)") + failed = true + return + } + print(pcmBuffer.format) + if pcmBuffer.frameLength == 0 { + // finished + } else { + // append buffer to file + let fileURL: URL + if isFullPath { + fileURL = URL(fileURLWithPath: fileName) + } else { + fileURL = FileManager.default.urls( + for: .documentDirectory, in: .userDomainMask + ).first! + .appendingPathComponent(fileName) + } + NSLog("Saving utterance to file: \(fileURL.absoluteString)") + + if output == nil { + do { + if #available(iOS 17.0, *) { + guard + let audioFormat = AVAudioFormat( + commonFormat: .pcmFormatFloat32, + sampleRate: pcmBuffer.format.sampleRate, + channels: 1, interleaved: false + ) + else { + NSLog("Error creating audio format for iOS 17+") + failed = true + return + } + output = try AVAudioFile( + forWriting: fileURL, settings: audioFormat.settings + ) + } else { + output = try AVAudioFile( + forWriting: fileURL, settings: pcmBuffer.format.settings, + commonFormat: .pcmFormatFloat32, interleaved: false + ) + } + } catch { + NSLog("Error creating AVAudioFile: \(error.localizedDescription)") + failed = true + return + } + } + + try! output!.write(from: pcmBuffer) + } + } + } else { + completion(Result.failure(PigeonError(code: FlutterTtsErrorCode.notSupportedOSVersion.toStrCode(), + message: kVoiceSelectionNotSuported, + details: nil))) + } + if failed { + completion(Result.success(TtsResult(success: false))) + } + if awaitSynthCompletion { + synthResult = completion + } else { + completion(Result.success(TtsResult(success: true))) + } + } + + private func pauseImpl(completion: ResultCallback) { + if synthesizer.pauseSpeaking(at: AVSpeechBoundary.word) { + completion(Result.success(TtsResult(success: true))) } else { - result(1) + completion(Result.success(TtsResult(success: false))) } - } else { - result(0) - } - } else { - let utterance = AVSpeechUtterance(string: text) - if self.voice != nil { - utterance.voice = self.voice! - } else { - utterance.voice = AVSpeechSynthesisVoice(language: self.language) - } - utterance.rate = self.rate - utterance.volume = self.volume - utterance.pitchMultiplier = self.pitch - - self.synthesizer.speak(utterance) - if self.awaitSpeakCompletion { - self.speakResult = result - } else { - result(1) - } - } - } - - private func synthesizeToFile(text: String, fileName: String, isFullPath: Bool, result: @escaping FlutterResult) { - var output: AVAudioFile? - var failed = false - let utterance = AVSpeechUtterance(string: text) - - if self.voice != nil { - utterance.voice = self.voice! - } else { - utterance.voice = AVSpeechSynthesisVoice(language: self.language) - } - utterance.rate = self.rate - utterance.volume = self.volume - utterance.pitchMultiplier = self.pitch - - if #available(iOS 13.0, *) { - self.synthesizer.write(utterance) { (buffer: AVAudioBuffer) in - guard let pcmBuffer = buffer as? AVAudioPCMBuffer else { - NSLog("unknow buffer type: \(buffer)") - failed = true - return + } + + private func setLanguageImpl(language: String, completion: ResultCallback) { + if !(languages.contains(where: { + $0.range(of: language, options: [.caseInsensitive, .anchored]) != nil + })) { + completion(Result.success(TtsResult(success: false))) + } else { + self.language = language + voice = nil + completion(Result.success(TtsResult(success: true))) } - print(pcmBuffer.format) - if pcmBuffer.frameLength == 0 { - // finished + } + + private func setRateImpl(rate: Float) { + self.rate = rate + } + + private func setVolumeImpl(volume: Float, completion: ResultCallback) { + if volume >= 0.0 && volume <= 1.0 { + self.volume = volume + completion(Result.success(TtsResult(success: true))) + } else { + completion(Result.success(TtsResult(success: false))) + } + } + + private func setPitchImpl(pitch: Float, completion: ResultCallback) { + if volume >= 0.5 && volume <= 2.0 { + self.pitch = pitch + completion(Result.success(TtsResult(success: true))) } else { - // append buffer to file - let fileURL: URL - if isFullPath { - fileURL = URL(fileURLWithPath: fileName) - } else { - fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(fileName) - } - NSLog("Saving utterance to file: \(fileURL.absoluteString)") - - if output == nil { - do { - if #available(iOS 17.0, *) { - guard let audioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: pcmBuffer.format.sampleRate, channels: 1, interleaved: false) else { - NSLog("Error creating audio format for iOS 17+") - failed = true - return - } - output = try AVAudioFile(forWriting: fileURL, settings: audioFormat.settings) + completion(Result.success(TtsResult(success: false))) + } + } + + private func setSharedInstanceImpl(sharedInstance: Bool, completion: ResultCallback) { + do { + try AVAudioSession.sharedInstance().setActive(sharedInstance) + completion(Result.success(TtsResult(success: true))) + } catch { + completion(Result.success(TtsResult(success: false))) + } + } + + private func setAudioCategoryImpl( + category: IosTextToSpeechAudioCategory, + audioOptions: [IosTextToSpeechAudioCategoryOptions], + mode: IosTextToSpeechAudioMode, + completion: ResultCallback + ) { + let category: AVAudioSession.Category = category.toAVAudioSessionCategory() + let options: AVAudioSession.CategoryOptions = audioOptions.reduce([]) { + completion, option -> AVAudioSession.CategoryOptions in + return completion.union(option.toAVAudioSessionCategoryOptions() ?? []) + } + + do { + if #available(iOS 12.0, *) { + let mode: AVAudioSession.Mode? = mode.toAVAudioSessionMode() ?? AVAudioSession.Mode.default + try audioSession.setCategory(category, mode: mode!, options: options) } else { - output = try AVAudioFile(forWriting: fileURL, settings: pcmBuffer.format.settings, commonFormat: .pcmFormatFloat32, interleaved: false) + try audioSession.setCategory(category, options: options) } - } catch { - NSLog("Error creating AVAudioFile: \(error.localizedDescription)") - failed = true - return - } + completion(Result.success(TtsResult(success: true))) + } catch { + print(error) + completion(Result.success(TtsResult(success: false))) } + } + private func stopImpl() { + synthesizer.stopSpeaking(at: AVSpeechBoundary.immediate) + } - try! output!.write(from: pcmBuffer) + private func getLanguagesImpl(completion: ResultCallback<[String]>) { + completion(Result.success(Array(languages))) + } + + private func getSpeechRateValidRangeImpl(completion: ResultCallback) { + let validSpeechRateRange = TtsRateValidRange(minimum: Double(AVSpeechUtteranceMinimumSpeechRate), + normal: Double(AVSpeechUtteranceDefaultSpeechRate), + maximum: Double(AVSpeechUtteranceMaximumSpeechRate), + platform: TtsPlatform.ios) + completion(Result.success(validSpeechRateRange)) + } + + private func isLanguageAvailableImpl(language: String, completion: ResultCallback) { + var isAvailable = false + if languages.contains(where: { + $0.range(of: language, options: [.caseInsensitive, .anchored]) != nil + }) { + isAvailable = true } - } - } else { - result("Unsupported iOS version") - } - if failed { - result(0) - } - if self.awaitSynthCompletion { - self.synthResult = result - } else { - result(1) - } - } - - private func pause(result: FlutterResult) { - if (self.synthesizer.pauseSpeaking(at: AVSpeechBoundary.word)) { - result(1) - } else { - result(0) - } - } - - private func setLanguage(language: String, result: FlutterResult) { - if !(self.languages.contains(where: {$0.range(of: language, options: [.caseInsensitive, .anchored]) != nil})) { - result(0) - } else { - self.language = language - self.voice = nil - result(1) - } - } - - private func setRate(rate: Float) { - self.rate = rate - } - - private func setVolume(volume: Float, result: FlutterResult) { - if (volume >= 0.0 && volume <= 1.0) { - self.volume = volume - result(1) - } else { - result(0) - } - } - - private func setPitch(pitch: Float, result: FlutterResult) { - if (volume >= 0.5 && volume <= 2.0) { - self.pitch = pitch - result(1) - } else { - result(0) - } - } - - private func setSharedInstance(sharedInstance: Bool, result: FlutterResult) { - do { - try AVAudioSession.sharedInstance().setActive(sharedInstance) - result(1) - } catch { - result(0) - } - } - - private func setAudioCategory(audioCategory: String?, audioOptions: Array?, audioMode: String?, result: FlutterResult){ - let category: AVAudioSession.Category = AudioCategory(rawValue: audioCategory ?? "")?.toAVAudioSessionCategory() ?? audioSession.category - let options: AVAudioSession.CategoryOptions = audioOptions?.reduce([], { (result, option) -> AVAudioSession.CategoryOptions in - return result.union(AudioCategoryOptions(rawValue: option)?.toAVAudioSessionCategoryOptions() ?? [])}) ?? [] - do { - if #available(iOS 12.0, *) { - if audioMode == nil { - try audioSession.setCategory(category, options: options) - } else { - let mode: AVAudioSession.Mode? = AudioModes(rawValue: audioMode ?? "")?.toAVAudioSessionMode() ?? AVAudioSession.Mode.default - try audioSession.setCategory(category, mode: mode!, options: options) + completion(Result.success(isAvailable)) + } + + private func getVoicesImpl(completion: ResultCallback<[Voice]>) { + if #available(iOS 9.0, *) { + var voices = [Voice]() + for voice in AVSpeechSynthesisVoice.speechVoices() { + var gender: String? = nil + if #available(iOS 13.0, *) { + gender = voice.gender.stringValue + } + + let voiceDict = Voice(name: voice.name, + locale: voice.language, + gender: gender, + quality: voice.quality.stringValue, + identifier: voice.identifier) + voices.append(voiceDict) } + completion(Result.success(voices)) } else { - try audioSession.setCategory(category, options: options) + completion(Result.failure(PigeonError(code: FlutterTtsErrorCode.notSupportedOSVersion.toStrCode(), + message: kVoiceSelectionNotSuported, + details: nil))) } - result(1) - } catch { - print(error) - result(0) - } - } - - private func stop() { - self.synthesizer.stopSpeaking(at: AVSpeechBoundary.immediate) - } - - private func getLanguages(result: FlutterResult) { - result(Array(self.languages)) - } - - private func getSpeechRateValidRange(result: FlutterResult) { - let validSpeechRateRange: [String:String] = [ - "min": String(AVSpeechUtteranceMinimumSpeechRate), - "normal": String(AVSpeechUtteranceDefaultSpeechRate), - "max": String(AVSpeechUtteranceMaximumSpeechRate), - "platform": "ios" - ] - result(validSpeechRateRange) - } - - private func isLanguageAvailable(language: String, result: FlutterResult) { - var isAvailable: Bool = false - if (self.languages.contains(where: {$0.range(of: language, options: [.caseInsensitive, .anchored]) != nil})) { - isAvailable = true - } - result(isAvailable); - } - - private func getVoices(result: FlutterResult) { - if #available(iOS 9.0, *) { - let voices = NSMutableArray() - var voiceDict: [String: String] = [:] - for voice in AVSpeechSynthesisVoice.speechVoices() { - voiceDict["name"] = voice.name - voiceDict["locale"] = voice.language - voiceDict["quality"] = voice.quality.stringValue - if #available(iOS 13.0, *) { - voiceDict["gender"] = voice.gender.stringValue + } + + private func setVoiceImpl(voice: Voice, completion: ResultCallback) { + if #available(iOS 9.0, *) { + // Check if identifier exists and is not empty + if let identifier = voice.identifier, !identifier.isEmpty { + // Find the voice by identifier + if let selectedVoice = AVSpeechSynthesisVoice(identifier: identifier) { + self.voice = selectedVoice + self.language = selectedVoice.language + completion(Result.success(TtsResult(success: true))) + return + } + } + + // If no valid identifier, search by name and locale, then prioritize by quality + let name = voice.name + let locale = voice.locale + let matchingVoices = AVSpeechSynthesisVoice.speechVoices().filter { + $0.name == name && $0.language == locale + } + + if !matchingVoices.isEmpty { + // Sort voices by quality: premium (if available) > enhanced > others + let sortedVoices = matchingVoices.sorted { voice1, voice2 -> Bool in + let quality1 = voice1.quality + let quality2 = voice2.quality + + // macOS 13.0+ supports premium quality + if #available(iOS 16.0, *) { + if quality1 == .premium { + return true + } else if quality1 == .enhanced && quality2 != .premium { + return true + } else { + return false + } + } else { + // Fallback for macOS versions before 13.0 (no premium) + if quality1 == .enhanced { + return true + } else { + return false + } + } + } + + // Select the highest quality voice + if let selectedVoice = sortedVoices.first { + self.voice = selectedVoice + self.language = selectedVoice.language + completion(Result.success(TtsResult(success: true))) + return + } + } + + // No matching voice found + completion(Result.success(TtsResult(success: false))) + } else { + completion(Result.failure(PigeonError(code: FlutterTtsErrorCode.notSupportedOSVersion.toStrCode(), + message: kVoiceSelectionNotSuported, + details: nil))) + } + } + + private func clearVoiceImpl() { + voice = nil + } + + private func shouldDeactivateAndNotifyOthers(_ session: AVAudioSession) -> Bool { + var options: AVAudioSession.CategoryOptions = .duckOthers + if #available(iOS 9.0, *) { + options.insert(.interruptSpokenAudioAndMixWithOthers) + } + options.remove(.mixWithOthers) + + return !options.isDisjoint(with: session.categoryOptions) + } + + public func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance + ) { + if shouldDeactivateAndNotifyOthers(audioSession) && autoStopSharedSessionImpl { + do { + try audioSession.setActive(false, options: .notifyOthersOnDeactivation) + } catch { + print(error) + } + } + if awaitSpeakCompletion && speakResult != nil { + speakResult!(Result.success(TtsResult(success: true))) + speakResult = nil } - voiceDict["identifier"] = voice.identifier - voices.add(voiceDict) - } - result(voices) - } else { - // Since voice selection is not supported below iOS 9, make voice getter and setter - // have the same bahavior as language selection. - getLanguages(result: result) - } - } - - private func setVoice(voice: [String: String], result: FlutterResult) { - if #available(iOS 9.0, *) { - // Check if identifier exists and is not empty - if let identifier = voice["identifier"], !identifier.isEmpty { - // Find the voice by identifier - if let selectedVoice = AVSpeechSynthesisVoice(identifier: identifier) { - self.voice = selectedVoice - self.language = selectedVoice.language - result(1) - return - } - } - - // If no valid identifier, search by name and locale, then prioritize by quality - if let name = voice["name"], let locale = voice["locale"] { - let matchingVoices = AVSpeechSynthesisVoice.speechVoices().filter { $0.name == name && $0.language == locale } - - if !matchingVoices.isEmpty { - // Sort voices by quality: premium (if available) > enhanced > others - let sortedVoices = matchingVoices.sorted { (voice1, voice2) -> Bool in - let quality1 = voice1.quality - let quality2 = voice2.quality - - // macOS 13.0+ supports premium quality - if #available(iOS 16.0, *) { - if quality1 == .premium { - return true - } else if quality1 == .enhanced && quality2 != .premium { - return true - } else { - return false - } - } else { - // Fallback for macOS versions before 13.0 (no premium) - if quality1 == .enhanced { - return true - } else { - return false - } - } - } - - // Select the highest quality voice - if let selectedVoice = sortedVoices.first { - self.voice = selectedVoice - self.language = selectedVoice.language - result(1) - return - } - } - } - - // No matching voice found - result(0) - } else { - // Handle older iOS versions if needed - setLanguage(language: voice["name"]!, result: result) - } - } - - private func clearVoice() { - self.voice = nil - } - - private func shouldDeactivateAndNotifyOthers(_ session: AVAudioSession) -> Bool { - var options: AVAudioSession.CategoryOptions = .duckOthers - if #available(iOS 9.0, *) { - options.insert(.interruptSpokenAudioAndMixWithOthers) - } - options.remove(.mixWithOthers) - - return !options.isDisjoint(with: session.categoryOptions) - } - - public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { - if shouldDeactivateAndNotifyOthers(audioSession) && self.autoStopSharedSession { - do { - try audioSession.setActive(false, options: .notifyOthersOnDeactivation) - } catch { - print(error) - } - } - if self.awaitSpeakCompletion && self.speakResult != nil { - self.speakResult!(1) - self.speakResult = nil - } - if self.awaitSynthCompletion && self.synthResult != nil { - self.synthResult!(1) - self.synthResult = nil - } - self.channel.invokeMethod("speak.onComplete", arguments: nil) - } - - public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) { - self.channel.invokeMethod("speak.onStart", arguments: nil) - } - - public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) { - self.channel.invokeMethod("speak.onPause", arguments: nil) - } - - public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) { - self.channel.invokeMethod("speak.onContinue", arguments: nil) - } - - public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) { - self.channel.invokeMethod("speak.onCancel", arguments: nil) - } - - public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) { - let nsWord = utterance.speechString as NSString - let data: [String:String] = [ - "text": utterance.speechString, - "start": String(characterRange.location), - "end": String(characterRange.location + characterRange.length), - "word": nsWord.substring(with: characterRange) - ] - self.channel.invokeMethod("speak.onProgress", arguments: data) - } + if awaitSynthCompletion && synthResult != nil { + synthResult!(Result.success(TtsResult(success: true))) + synthResult = nil + } + + flutterApi.onSpeakCompleteCb { _ in } + } + public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) { + flutterApi.onSpeakStartCb { _ in } + } + + public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) { + flutterApi.onSpeakPauseCb { _ in } + } + + public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) { + flutterApi.onSpeakResumeCb { _ in } + } + + public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) { + flutterApi.onSpeakCancelCb { _ in } + } + + public func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, + utterance: AVSpeechUtterance + ) { + let nsWord = utterance.speechString as NSString + let data = TtsProgress( + text: utterance.speechString, + start: Int64(characterRange.location), + end: Int64(characterRange.location + characterRange.length), + word: nsWord.substring(with: characterRange) + ) + + flutterApi.onSpeakProgressCb(progress: data) { _ in } + } } extension AVSpeechSynthesisVoiceQuality { diff --git a/ios/Classes/message.g.swift b/ios/Classes/message.g.swift new file mode 100644 index 00000000..d2539406 --- /dev/null +++ b/ios/Classes/message.g.swift @@ -0,0 +1,1548 @@ +// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Sendable? + + init(code: String, message: String?, details: Sendable?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func createConnectionError(withChannelName channelName: String) -> PigeonError { + return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "") +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +func deepEqualsmessage(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case is (Void, Void): + return true + + case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): + return cleanLhsHashable == cleanRhsHashable + + case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): + guard cleanLhsArray.count == cleanRhsArray.count else { return false } + for (index, element) in cleanLhsArray.enumerated() { + if !deepEqualsmessage(element, cleanRhsArray[index]) { + return false + } + } + return true + + case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } + for (key, cleanLhsValue) in cleanLhsDictionary { + guard cleanRhsDictionary.index(forKey: key) != nil else { return false } + if !deepEqualsmessage(cleanLhsValue, cleanRhsDictionary[key]!) { + return false + } + } + return true + + default: + // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. + return false + } +} + +func deepHashmessage(value: Any?, hasher: inout Hasher) { + if let valueList = value as? [AnyHashable] { + for item in valueList { deepHashmessage(value: item, hasher: &hasher) } + return + } + + if let valueDict = value as? [AnyHashable: AnyHashable] { + for key in valueDict.keys { + hasher.combine(key) + deepHashmessage(value: valueDict[key]!, hasher: &hasher) + } + return + } + + if let hashableValue = value as? AnyHashable { + hasher.combine(hashableValue.hashValue) + } + + return hasher.combine(String(describing: value)) +} + + + +enum FlutterTtsErrorCode: Int { + /// general error code for TTS engine not available. + case ttsNotAvailable = 0 + /// The TTS engine failed to initialize in n second. + /// 1 second is the default timeout. + /// e.g. Some Android custom ROMS may trim TTS service, + /// and third party TTS engine may fail to initialize due to battery optimization. + case ttsInitTimeout = 1 + /// not supported on current os version + case notSupportedOSVersion = 2 +} + +/// Audio session category identifiers for iOS. +/// +/// See also: +/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/category +enum IosTextToSpeechAudioCategory: Int { + /// The default audio session category. + /// + /// Your audio is silenced by screen locking and by the Silent switch. + /// + /// By default, using this category implies that your app’s audio + /// is nonmixable—activating your session will interrupt + /// any other audio sessions which are also nonmixable. + /// To allow mixing, use the [ambient] category instead. + case ambientSolo = 0 + /// The category for an app in which sound playback is nonprimary — that is, + /// your app also works with the sound turned off. + /// + /// This category is also appropriate for “play-along” apps, + /// such as a virtual piano that a user plays while the Music app is playing. + /// When you use this category, audio from other apps mixes with your audio. + /// Screen locking and the Silent switch (on iPhone, the Ring/Silent switch) silence your audio. + case ambient = 1 + /// The category for playing recorded music or other sounds + /// that are central to the successful use of your app. + /// + /// When using this category, your app audio continues + /// with the Silent switch set to silent or when the screen locks. + /// + /// By default, using this category implies that your app’s audio + /// is nonmixable—activating your session will interrupt + /// any other audio sessions which are also nonmixable. + /// To allow mixing for this category, use the + /// [IosTextToSpeechAudioCategoryOptions.mixWithOthers] option. + case playback = 2 + /// The category for recording (input) and playback (output) of audio, + /// such as for a Voice over Internet Protocol (VoIP) app. + /// + /// Your audio continues with the Silent switch set to silent and with the screen locked. + /// This category is appropriate for simultaneous recording and playback, + /// and also for apps that record and play back, but not simultaneously. + case playAndRecord = 3 +} + +/// Audio session mode identifiers for iOS. +/// +/// See also: +/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/mode +enum IosTextToSpeechAudioMode: Int { + /// The default audio session mode. + /// + /// You can use this mode with every [IosTextToSpeechAudioCategory]. + case defaultMode = 0 + /// A mode that the GameKit framework sets on behalf of an application + /// that uses GameKit’s voice chat service. + /// + /// This mode is valid only with the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// + /// Don’t set this mode directly. If you need similar behavior and aren’t + /// using a `GKVoiceChat` object, use [voiceChat] or [videoChat] instead. + case gameChat = 1 + /// A mode that indicates that your app is performing measurement of audio input or output. + /// + /// Use this mode for apps that need to minimize the amount of + /// system-supplied signal processing to input and output signals. + /// If recording on devices with more than one built-in microphone, + /// the session uses the primary microphone. + /// + /// For use with the [IosTextToSpeechAudioCategory.playback] or + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// + /// **Important:** This mode disables some dynamics processing on input and output signals, + /// resulting in a lower-output playback level. + case measurement = 2 + /// A mode that indicates that your app is playing back movie content. + /// + /// When you set this mode, the audio session uses signal processing to enhance + /// movie playback for certain audio routes such as built-in speaker or headphones. + /// You may only use this mode with the + /// [IosTextToSpeechAudioCategory.playback] category. + case moviePlayback = 3 + /// A mode used for continuous spoken audio to pause the audio when another app plays a short audio prompt. + /// + /// This mode is appropriate for apps that play continuous spoken audio, + /// such as podcasts or audio books. Setting this mode indicates that your app + /// should pause, rather than duck, its audio if another app plays + /// a spoken audio prompt. After the interrupting app’s audio ends, you can + /// resume your app’s audio playback. + case spokenAudio = 4 + /// A mode that indicates that your app is engaging in online video conferencing. + /// + /// Use this mode for video chat apps that use the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// When you set this mode, the audio session optimizes the device’s tonal + /// equalization for voice. It also reduces the set of allowable audio routes + /// to only those appropriate for video chat. + /// + /// Using this mode has the side effect of enabling the + /// [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. + case videoChat = 5 + /// A mode that indicates that your app is recording a movie. + /// + /// This mode is valid only with the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// On devices with more than one built-in microphone, + /// the audio session uses the microphone closest to the video camera. + /// + /// Use this mode to ensure that the system provides appropriate audio-signal processing. + case videoRecording = 6 + /// A mode that indicates that your app is performing two-way voice communication, + /// such as using Voice over Internet Protocol (VoIP). + /// + /// Use this mode for Voice over IP (VoIP) apps that use the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// When you set this mode, the session optimizes the device’s tonal + /// equalization for voice and reduces the set of allowable audio routes + /// to only those appropriate for voice chat. + /// + /// Using this mode has the side effect of enabling the + /// [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. + case voiceChat = 7 + /// A mode that indicates that your app plays audio using text-to-speech. + /// + /// Setting this mode allows for different routing behaviors when your app + /// is connected to certain audio devices, such as CarPlay. + /// An example of an app that uses this mode is a turn-by-turn navigation app + /// that plays short prompts to the user. + /// + /// Typically, apps of the same type also configure their sessions to use the + /// [IosTextToSpeechAudioCategoryOptions.duckOthers] and + /// [IosTextToSpeechAudioCategoryOptions.interruptSpokenAudioAndMixWithOthers] options. + case voicePrompt = 8 +} + +/// Audio session category options for iOS. +/// +/// See also: +/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions +enum IosTextToSpeechAudioCategoryOptions: Int { + /// An option that indicates whether audio from this session mixes with audio + /// from active sessions in other audio apps. + /// + /// You can set this option explicitly only if the audio session category + /// is [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// If you set the audio session category to [IosTextToSpeechAudioCategory.ambient], + /// the session automatically sets this option. + /// Likewise, setting the [duckOthers] or [interruptSpokenAudioAndMixWithOthers] + /// options also enables this option. + /// + /// If you set this option, your app mixes its audio with audio playing + /// in background apps, such as the Music app. + case mixWithOthers = 0 + /// An option that reduces the volume of other audio sessions while audio + /// from this session plays. + /// + /// You can set this option only if the audio session category is + /// [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// Setting it implicitly sets the [mixWithOthers] option. + /// + /// Use this option to mix your app’s audio with that of others. + /// While your app plays its audio, the system reduces the volume of other + /// audio sessions to make yours more prominent. If your app provides + /// occasional spoken audio, such as in a turn-by-turn navigation app + /// or an exercise app, you should also set the [interruptSpokenAudioAndMixWithOthers] option. + /// + /// Note that ducking begins when you activate your app’s audio session + /// and ends when you deactivate the session. + /// + /// See also: + /// * [FlutterTts.setSharedInstance] + case duckOthers = 1 + /// An option that determines whether to pause spoken audio content + /// from other sessions when your app plays its audio. + /// + /// You can set this option only if the audio session category is + /// [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// Setting this option also sets [mixWithOthers]. + /// + /// If you set this option, the system mixes your audio with other + /// audio sessions, but interrupts (and stops) audio sessions that use the + /// [IosTextToSpeechAudioMode.spokenAudio] audio session mode. + /// It pauses the audio from other apps as long as your session is active. + /// After your audio session deactivates, the system resumes the interrupted app’s audio. + /// + /// Set this option if your app’s audio is occasional and spoken, + /// such as in a turn-by-turn navigation app or an exercise app. + /// This avoids intelligibility problems when two spoken audio apps mix. + /// If you set this option, also set the [duckOthers] option unless + /// you have a specific reason not to. Ducking other audio, rather than + /// interrupting it, is appropriate when the other audio isn’t spoken audio. + case interruptSpokenAudioAndMixWithOthers = 2 + /// An option that determines whether Bluetooth hands-free devices appear + /// as available input routes. + /// + /// You can set this option only if the audio session category is + /// [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// + /// You’re required to set this option to allow routing audio input and output + /// to a paired Bluetooth Hands-Free Profile (HFP) device. + /// If you clear this option, paired Bluetooth HFP devices don’t show up + /// as available audio input routes. + case allowBluetooth = 3 + /// An option that determines whether you can stream audio from this session + /// to Bluetooth devices that support the Advanced Audio Distribution Profile (A2DP). + /// + /// A2DP is a stereo, output-only profile intended for higher bandwidth + /// audio use cases, such as music playback. + /// The system automatically routes to A2DP ports if you configure an + /// app’s audio session to use the [IosTextToSpeechAudioCategory.ambient], + /// [IosTextToSpeechAudioCategory.ambientSolo], or + /// [IosTextToSpeechAudioCategory.playback] categories. + /// + /// Starting with iOS 10.0, apps using the + /// [IosTextToSpeechAudioCategory.playAndRecord] category may also allow + /// routing output to paired Bluetooth A2DP devices. To enable this behavior, + /// pass this category option when setting your audio session’s category. + /// + /// Note: If this option and the [allowBluetooth] option are both set, + /// when a single device supports both the Hands-Free Profile (HFP) and A2DP, + /// the system gives hands-free ports a higher priority for routing. + case allowBluetoothA2DP = 4 + /// An option that determines whether you can stream audio + /// from this session to AirPlay devices. + /// + /// Setting this option enables the audio session to route audio output + /// to AirPlay devices. You can only explicitly set this option if the + /// audio session’s category is set to [IosTextToSpeechAudioCategory.playAndRecord]. + /// For most other audio session categories, the system sets this option implicitly. + case allowAirPlay = 5 + /// An option that determines whether audio from the session defaults to the built-in speaker instead of the receiver. + /// + /// You can set this option only when using the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// It’s used to modify the category’s routing behavior so that audio + /// is always routed to the speaker rather than the receiver if + /// no other accessories, such as headphones, are in use. + /// + /// When using this option, the system honors user gestures. + /// For example, plugging in a headset causes the route to change to + /// headset mic/headphones, and unplugging the headset causes the route + /// to change to built-in mic/speaker (as opposed to built-in mic/receiver) + /// when you’ve set this override. + /// + /// In the case of using a USB input-only accessory, audio input + /// comes from the accessory, and the system routes audio to the headphones, + /// if attached, or to the speaker if the headphones aren’t plugged in. + /// The use case is to route audio to the speaker instead of the receiver + /// in cases where the audio would normally go to the receiver. + case defaultToSpeaker = 6 +} + +enum TtsPlatform: Int { + case android = 0 + case ios = 1 +} + +/// Generated class from Pigeon that represents data sent in messages. +struct Voice: Hashable { + var name: String + var locale: String + var gender: String? = nil + var quality: String? = nil + var identifier: String? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> Voice? { + let name = pigeonVar_list[0] as! String + let locale = pigeonVar_list[1] as! String + let gender: String? = nilOrValue(pigeonVar_list[2]) + let quality: String? = nilOrValue(pigeonVar_list[3]) + let identifier: String? = nilOrValue(pigeonVar_list[4]) + + return Voice( + name: name, + locale: locale, + gender: gender, + quality: quality, + identifier: identifier + ) + } + func toList() -> [Any?] { + return [ + name, + locale, + gender, + quality, + identifier, + ] + } + static func == (lhs: Voice, rhs: Voice) -> Bool { + return deepEqualsmessage(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashmessage(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct TtsResult: Hashable { + var success: Bool + var message: String? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> TtsResult? { + let success = pigeonVar_list[0] as! Bool + let message: String? = nilOrValue(pigeonVar_list[1]) + + return TtsResult( + success: success, + message: message + ) + } + func toList() -> [Any?] { + return [ + success, + message, + ] + } + static func == (lhs: TtsResult, rhs: TtsResult) -> Bool { + return deepEqualsmessage(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashmessage(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct TtsProgress: Hashable { + var text: String + var start: Int64 + var end: Int64 + var word: String + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> TtsProgress? { + let text = pigeonVar_list[0] as! String + let start = pigeonVar_list[1] as! Int64 + let end = pigeonVar_list[2] as! Int64 + let word = pigeonVar_list[3] as! String + + return TtsProgress( + text: text, + start: start, + end: end, + word: word + ) + } + func toList() -> [Any?] { + return [ + text, + start, + end, + word, + ] + } + static func == (lhs: TtsProgress, rhs: TtsProgress) -> Bool { + return deepEqualsmessage(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashmessage(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct TtsRateValidRange: Hashable { + var minimum: Double + var normal: Double + var maximum: Double + var platform: TtsPlatform + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> TtsRateValidRange? { + let minimum = pigeonVar_list[0] as! Double + let normal = pigeonVar_list[1] as! Double + let maximum = pigeonVar_list[2] as! Double + let platform = pigeonVar_list[3] as! TtsPlatform + + return TtsRateValidRange( + minimum: minimum, + normal: normal, + maximum: maximum, + platform: platform + ) + } + func toList() -> [Any?] { + return [ + minimum, + normal, + maximum, + platform, + ] + } + static func == (lhs: TtsRateValidRange, rhs: TtsRateValidRange) -> Bool { + return deepEqualsmessage(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashmessage(value: toList(), hasher: &hasher) + } +} + +private class MessagePigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return FlutterTtsErrorCode(rawValue: enumResultAsInt) + } + return nil + case 130: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return IosTextToSpeechAudioCategory(rawValue: enumResultAsInt) + } + return nil + case 131: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return IosTextToSpeechAudioMode(rawValue: enumResultAsInt) + } + return nil + case 132: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return IosTextToSpeechAudioCategoryOptions(rawValue: enumResultAsInt) + } + return nil + case 133: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return TtsPlatform(rawValue: enumResultAsInt) + } + return nil + case 134: + return Voice.fromList(self.readValue() as! [Any?]) + case 135: + return TtsResult.fromList(self.readValue() as! [Any?]) + case 136: + return TtsProgress.fromList(self.readValue() as! [Any?]) + case 137: + return TtsRateValidRange.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class MessagePigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? FlutterTtsErrorCode { + super.writeByte(129) + super.writeValue(value.rawValue) + } else if let value = value as? IosTextToSpeechAudioCategory { + super.writeByte(130) + super.writeValue(value.rawValue) + } else if let value = value as? IosTextToSpeechAudioMode { + super.writeByte(131) + super.writeValue(value.rawValue) + } else if let value = value as? IosTextToSpeechAudioCategoryOptions { + super.writeByte(132) + super.writeValue(value.rawValue) + } else if let value = value as? TtsPlatform { + super.writeByte(133) + super.writeValue(value.rawValue) + } else if let value = value as? Voice { + super.writeByte(134) + super.writeValue(value.toList()) + } else if let value = value as? TtsResult { + super.writeByte(135) + super.writeValue(value.toList()) + } else if let value = value as? TtsProgress { + super.writeByte(136) + super.writeValue(value.toList()) + } else if let value = value as? TtsRateValidRange { + super.writeByte(137) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class MessagePigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return MessagePigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return MessagePigeonCodecWriter(data: data) + } +} + +class MessagePigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = MessagePigeonCodec(readerWriter: MessagePigeonCodecReaderWriter()) +} + + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol TtsHostApi { + func speak(text: String, forceFocus: Bool, completion: @escaping (Result) -> Void) + func pause(completion: @escaping (Result) -> Void) + func stop(completion: @escaping (Result) -> Void) + func setSpeechRate(rate: Double, completion: @escaping (Result) -> Void) + func setVolume(volume: Double, completion: @escaping (Result) -> Void) + func setPitch(pitch: Double, completion: @escaping (Result) -> Void) + func setVoice(voice: Voice, completion: @escaping (Result) -> Void) + func clearVoice(completion: @escaping (Result) -> Void) + func awaitSpeakCompletion(awaitCompletion: Bool, completion: @escaping (Result) -> Void) + func getLanguages(completion: @escaping (Result<[String], Error>) -> Void) + func getVoices(completion: @escaping (Result<[Voice], Error>) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class TtsHostApiSetup { + static var codec: FlutterStandardMessageCodec { MessagePigeonCodec.shared } + /// Sets up an instance of `TtsHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: TtsHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let speakChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.speak\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + speakChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let textArg = args[0] as! String + let forceFocusArg = args[1] as! Bool + api.speak(text: textArg, forceFocus: forceFocusArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + speakChannel.setMessageHandler(nil) + } + let pauseChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.pause\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + pauseChannel.setMessageHandler { _, reply in + api.pause { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + pauseChannel.setMessageHandler(nil) + } + let stopChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.stop\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + stopChannel.setMessageHandler { _, reply in + api.stop { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + stopChannel.setMessageHandler(nil) + } + let setSpeechRateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.setSpeechRate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setSpeechRateChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let rateArg = args[0] as! Double + api.setSpeechRate(rate: rateArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setSpeechRateChannel.setMessageHandler(nil) + } + let setVolumeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.setVolume\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setVolumeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let volumeArg = args[0] as! Double + api.setVolume(volume: volumeArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setVolumeChannel.setMessageHandler(nil) + } + let setPitchChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.setPitch\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setPitchChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let pitchArg = args[0] as! Double + api.setPitch(pitch: pitchArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setPitchChannel.setMessageHandler(nil) + } + let setVoiceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.setVoice\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setVoiceChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let voiceArg = args[0] as! Voice + api.setVoice(voice: voiceArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setVoiceChannel.setMessageHandler(nil) + } + let clearVoiceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.clearVoice\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + clearVoiceChannel.setMessageHandler { _, reply in + api.clearVoice { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + clearVoiceChannel.setMessageHandler(nil) + } + let awaitSpeakCompletionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.awaitSpeakCompletion\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + awaitSpeakCompletionChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let awaitCompletionArg = args[0] as! Bool + api.awaitSpeakCompletion(awaitCompletion: awaitCompletionArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + awaitSpeakCompletionChannel.setMessageHandler(nil) + } + let getLanguagesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.getLanguages\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getLanguagesChannel.setMessageHandler { _, reply in + api.getLanguages { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getLanguagesChannel.setMessageHandler(nil) + } + let getVoicesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.getVoices\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getVoicesChannel.setMessageHandler { _, reply in + api.getVoices { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getVoicesChannel.setMessageHandler(nil) + } + } +} +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol IosTtsHostApi { + func awaitSynthCompletion(awaitCompletion: Bool, completion: @escaping (Result) -> Void) + func synthesizeToFile(text: String, fileName: String, isFullPath: Bool, completion: @escaping (Result) -> Void) + func setSharedInstance(sharedSession: Bool, completion: @escaping (Result) -> Void) + func autoStopSharedSession(autoStop: Bool, completion: @escaping (Result) -> Void) + func setIosAudioCategory(category: IosTextToSpeechAudioCategory, options: [IosTextToSpeechAudioCategoryOptions], mode: IosTextToSpeechAudioMode, completion: @escaping (Result) -> Void) + func getSpeechRateValidRange(completion: @escaping (Result) -> Void) + func isLanguageAvailable(language: String, completion: @escaping (Result) -> Void) + func setLanguange(language: String, completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class IosTtsHostApiSetup { + static var codec: FlutterStandardMessageCodec { MessagePigeonCodec.shared } + /// Sets up an instance of `IosTtsHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: IosTtsHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let awaitSynthCompletionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.awaitSynthCompletion\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + awaitSynthCompletionChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let awaitCompletionArg = args[0] as! Bool + api.awaitSynthCompletion(awaitCompletion: awaitCompletionArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + awaitSynthCompletionChannel.setMessageHandler(nil) + } + let synthesizeToFileChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.synthesizeToFile\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + synthesizeToFileChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let textArg = args[0] as! String + let fileNameArg = args[1] as! String + let isFullPathArg = args[2] as! Bool + api.synthesizeToFile(text: textArg, fileName: fileNameArg, isFullPath: isFullPathArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + synthesizeToFileChannel.setMessageHandler(nil) + } + let setSharedInstanceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setSharedInstance\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setSharedInstanceChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let sharedSessionArg = args[0] as! Bool + api.setSharedInstance(sharedSession: sharedSessionArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setSharedInstanceChannel.setMessageHandler(nil) + } + let autoStopSharedSessionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.autoStopSharedSession\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + autoStopSharedSessionChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let autoStopArg = args[0] as! Bool + api.autoStopSharedSession(autoStop: autoStopArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + autoStopSharedSessionChannel.setMessageHandler(nil) + } + let setIosAudioCategoryChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setIosAudioCategory\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setIosAudioCategoryChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let categoryArg = args[0] as! IosTextToSpeechAudioCategory + let optionsArg = args[1] as! [IosTextToSpeechAudioCategoryOptions] + let modeArg = args[2] as! IosTextToSpeechAudioMode + api.setIosAudioCategory(category: categoryArg, options: optionsArg, mode: modeArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setIosAudioCategoryChannel.setMessageHandler(nil) + } + let getSpeechRateValidRangeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.getSpeechRateValidRange\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getSpeechRateValidRangeChannel.setMessageHandler { _, reply in + api.getSpeechRateValidRange { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getSpeechRateValidRangeChannel.setMessageHandler(nil) + } + let isLanguageAvailableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.isLanguageAvailable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isLanguageAvailableChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languageArg = args[0] as! String + api.isLanguageAvailable(language: languageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + isLanguageAvailableChannel.setMessageHandler(nil) + } + let setLanguangeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setLanguange\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setLanguangeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languageArg = args[0] as! String + api.setLanguange(language: languageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setLanguangeChannel.setMessageHandler(nil) + } + } +} +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol AndroidTtsHostApi { + func awaitSynthCompletion(awaitCompletion: Bool, completion: @escaping (Result) -> Void) + func getMaxSpeechInputLength(completion: @escaping (Result) -> Void) + func setEngine(engine: String, completion: @escaping (Result) -> Void) + func getEngines(completion: @escaping (Result<[String], Error>) -> Void) + func getDefaultEngine(completion: @escaping (Result) -> Void) + func getDefaultVoice(completion: @escaping (Result) -> Void) + /// [Future] which invokes the platform specific method for synthesizeToFile + func synthesizeToFile(text: String, fileName: String, isFullPath: Bool, completion: @escaping (Result) -> Void) + func isLanguageInstalled(language: String, completion: @escaping (Result) -> Void) + func isLanguageAvailable(language: String, completion: @escaping (Result) -> Void) + func areLanguagesInstalled(languages: [String], completion: @escaping (Result<[String: Bool], Error>) -> Void) + func getSpeechRateValidRange(completion: @escaping (Result) -> Void) + func setSilence(timems: Int64, completion: @escaping (Result) -> Void) + func setQueueMode(queueMode: Int64, completion: @escaping (Result) -> Void) + func setAudioAttributesForNavigation(completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class AndroidTtsHostApiSetup { + static var codec: FlutterStandardMessageCodec { MessagePigeonCodec.shared } + /// Sets up an instance of `AndroidTtsHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: AndroidTtsHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let awaitSynthCompletionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.awaitSynthCompletion\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + awaitSynthCompletionChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let awaitCompletionArg = args[0] as! Bool + api.awaitSynthCompletion(awaitCompletion: awaitCompletionArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + awaitSynthCompletionChannel.setMessageHandler(nil) + } + let getMaxSpeechInputLengthChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getMaxSpeechInputLength\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getMaxSpeechInputLengthChannel.setMessageHandler { _, reply in + api.getMaxSpeechInputLength { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getMaxSpeechInputLengthChannel.setMessageHandler(nil) + } + let setEngineChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setEngine\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setEngineChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let engineArg = args[0] as! String + api.setEngine(engine: engineArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setEngineChannel.setMessageHandler(nil) + } + let getEnginesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getEngines\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getEnginesChannel.setMessageHandler { _, reply in + api.getEngines { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getEnginesChannel.setMessageHandler(nil) + } + let getDefaultEngineChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getDefaultEngine\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getDefaultEngineChannel.setMessageHandler { _, reply in + api.getDefaultEngine { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getDefaultEngineChannel.setMessageHandler(nil) + } + let getDefaultVoiceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getDefaultVoice\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getDefaultVoiceChannel.setMessageHandler { _, reply in + api.getDefaultVoice { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getDefaultVoiceChannel.setMessageHandler(nil) + } + /// [Future] which invokes the platform specific method for synthesizeToFile + let synthesizeToFileChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.synthesizeToFile\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + synthesizeToFileChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let textArg = args[0] as! String + let fileNameArg = args[1] as! String + let isFullPathArg = args[2] as! Bool + api.synthesizeToFile(text: textArg, fileName: fileNameArg, isFullPath: isFullPathArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + synthesizeToFileChannel.setMessageHandler(nil) + } + let isLanguageInstalledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.isLanguageInstalled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isLanguageInstalledChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languageArg = args[0] as! String + api.isLanguageInstalled(language: languageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + isLanguageInstalledChannel.setMessageHandler(nil) + } + let isLanguageAvailableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.isLanguageAvailable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isLanguageAvailableChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languageArg = args[0] as! String + api.isLanguageAvailable(language: languageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + isLanguageAvailableChannel.setMessageHandler(nil) + } + let areLanguagesInstalledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.areLanguagesInstalled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + areLanguagesInstalledChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languagesArg = args[0] as! [String] + api.areLanguagesInstalled(languages: languagesArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + areLanguagesInstalledChannel.setMessageHandler(nil) + } + let getSpeechRateValidRangeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getSpeechRateValidRange\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getSpeechRateValidRangeChannel.setMessageHandler { _, reply in + api.getSpeechRateValidRange { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getSpeechRateValidRangeChannel.setMessageHandler(nil) + } + let setSilenceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setSilence\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setSilenceChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let timemsArg = args[0] as! Int64 + api.setSilence(timems: timemsArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setSilenceChannel.setMessageHandler(nil) + } + let setQueueModeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setQueueMode\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setQueueModeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let queueModeArg = args[0] as! Int64 + api.setQueueMode(queueMode: queueModeArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setQueueModeChannel.setMessageHandler(nil) + } + let setAudioAttributesForNavigationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setAudioAttributesForNavigation\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setAudioAttributesForNavigationChannel.setMessageHandler { _, reply in + api.setAudioAttributesForNavigation { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setAudioAttributesForNavigationChannel.setMessageHandler(nil) + } + } +} +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol MacosTtsHostApi { + func awaitSynthCompletion(awaitCompletion: Bool, completion: @escaping (Result) -> Void) + func getSpeechRateValidRange(completion: @escaping (Result) -> Void) + func setLanguange(language: String, completion: @escaping (Result) -> Void) + func isLanguageAvailable(language: String, completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class MacosTtsHostApiSetup { + static var codec: FlutterStandardMessageCodec { MessagePigeonCodec.shared } + /// Sets up an instance of `MacosTtsHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: MacosTtsHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let awaitSynthCompletionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.awaitSynthCompletion\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + awaitSynthCompletionChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let awaitCompletionArg = args[0] as! Bool + api.awaitSynthCompletion(awaitCompletion: awaitCompletionArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + awaitSynthCompletionChannel.setMessageHandler(nil) + } + let getSpeechRateValidRangeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.getSpeechRateValidRange\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getSpeechRateValidRangeChannel.setMessageHandler { _, reply in + api.getSpeechRateValidRange { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getSpeechRateValidRangeChannel.setMessageHandler(nil) + } + let setLanguangeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.setLanguange\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setLanguangeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languageArg = args[0] as! String + api.setLanguange(language: languageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setLanguangeChannel.setMessageHandler(nil) + } + let isLanguageAvailableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.isLanguageAvailable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isLanguageAvailableChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languageArg = args[0] as! String + api.isLanguageAvailable(language: languageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + isLanguageAvailableChannel.setMessageHandler(nil) + } + } +} +/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. +protocol TtsFlutterApiProtocol { + func onSpeakStartCb(completion: @escaping (Result) -> Void) + func onSpeakCompleteCb(completion: @escaping (Result) -> Void) + func onSpeakPauseCb(completion: @escaping (Result) -> Void) + func onSpeakResumeCb(completion: @escaping (Result) -> Void) + func onSpeakCancelCb(completion: @escaping (Result) -> Void) + func onSpeakProgressCb(progress progressArg: TtsProgress, completion: @escaping (Result) -> Void) + func onSpeakErrorCb(error errorArg: String, completion: @escaping (Result) -> Void) + func onSynthStartCb(completion: @escaping (Result) -> Void) + func onSynthCompleteCb(completion: @escaping (Result) -> Void) + func onSynthErrorCb(error errorArg: String, completion: @escaping (Result) -> Void) +} +class TtsFlutterApi: TtsFlutterApiProtocol { + private let binaryMessenger: FlutterBinaryMessenger + private let messageChannelSuffix: String + init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") { + self.binaryMessenger = binaryMessenger + self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + } + var codec: MessagePigeonCodec { + return MessagePigeonCodec.shared + } + func onSpeakStartCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakStartCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSpeakCompleteCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakCompleteCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSpeakPauseCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakPauseCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSpeakResumeCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakResumeCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSpeakCancelCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakCancelCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSpeakProgressCb(progress progressArg: TtsProgress, completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakProgressCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([progressArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSpeakErrorCb(error errorArg: String, completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakErrorCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([errorArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSynthStartCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthStartCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSynthCompleteCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthCompleteCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSynthErrorCb(error errorArg: String, completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthErrorCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([errorArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } +} diff --git a/ios/flutter_tts.podspec b/ios/flutter_tts.podspec index 03099f45..5aab5b43 100644 --- a/ios/flutter_tts.podspec +++ b/ios/flutter_tts.podspec @@ -13,7 +13,6 @@ A flutter text to speech plugin s.author = { 'tundralabs' => 'eyedea32@gmail.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' s.ios.deployment_target = '8.0' s.swift_version = '4.2' diff --git a/lib/flutter_tts.dart b/lib/flutter_tts.dart index 0d2d2f2a..4ed60652 100644 --- a/lib/flutter_tts.dart +++ b/lib/flutter_tts.dart @@ -1,666 +1,5 @@ -import 'dart:async'; -import 'dart:io' show Platform; - -import 'package:flutter/services.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; - -typedef ErrorHandler = void Function(dynamic message); -typedef ProgressHandler = void Function( - String text, int start, int end, String word); - -const String iosAudioCategoryOptionsKey = 'iosAudioCategoryOptionsKey'; -const String iosAudioCategoryKey = 'iosAudioCategoryKey'; -const String iosAudioModeKey = 'iosAudioModeKey'; -const String iosAudioCategoryAmbientSolo = 'iosAudioCategoryAmbientSolo'; -const String iosAudioCategoryAmbient = 'iosAudioCategoryAmbient'; -const String iosAudioCategoryPlayback = 'iosAudioCategoryPlayback'; -const String iosAudioCategoryPlaybackAndRecord = - 'iosAudioCategoryPlaybackAndRecord'; - -const String iosAudioCategoryOptionsMixWithOthers = - 'iosAudioCategoryOptionsMixWithOthers'; -const String iosAudioCategoryOptionsDuckOthers = - 'iosAudioCategoryOptionsDuckOthers'; -const String iosAudioCategoryOptionsInterruptSpokenAudioAndMixWithOthers = - 'iosAudioCategoryOptionsInterruptSpokenAudioAndMixWithOthers'; -const String iosAudioCategoryOptionsAllowBluetooth = - 'iosAudioCategoryOptionsAllowBluetooth'; -const String iosAudioCategoryOptionsAllowBluetoothA2DP = - 'iosAudioCategoryOptionsAllowBluetoothA2DP'; -const String iosAudioCategoryOptionsAllowAirPlay = - 'iosAudioCategoryOptionsAllowAirPlay'; -const String iosAudioCategoryOptionsDefaultToSpeaker = - 'iosAudioCategoryOptionsDefaultToSpeaker'; - -const String iosAudioModeDefault = 'iosAudioModeDefault'; -const String iosAudioModeGameChat = 'iosAudioModeGameChat'; -const String iosAudioModeMeasurement = 'iosAudioModeMeasurement'; -const String iosAudioModeMoviePlayback = 'iosAudioModeMoviePlayback'; -const String iosAudioModeSpokenAudio = 'iosAudioModeSpokenAudio'; -const String iosAudioModeVideoChat = 'iosAudioModeVideoChat'; -const String iosAudioModeVideoRecording = 'iosAudioModeVideoRecording'; -const String iosAudioModeVoiceChat = 'iosAudioModeVoiceChat'; -const String iosAudioModeVoicePrompt = 'iosAudioModeVoicePrompt'; - -enum TextToSpeechPlatform { android, ios } - -/// Audio session category identifiers for iOS. -/// -/// See also: -/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/category -enum IosTextToSpeechAudioCategory { - /// The default audio session category. - /// - /// Your audio is silenced by screen locking and by the Silent switch. - /// - /// By default, using this category implies that your app’s audio - /// is nonmixable—activating your session will interrupt - /// any other audio sessions which are also nonmixable. - /// To allow mixing, use the [ambient] category instead. - ambientSolo, - - /// The category for an app in which sound playback is nonprimary — that is, - /// your app also works with the sound turned off. - /// - /// This category is also appropriate for “play-along” apps, - /// such as a virtual piano that a user plays while the Music app is playing. - /// When you use this category, audio from other apps mixes with your audio. - /// Screen locking and the Silent switch (on iPhone, the Ring/Silent switch) silence your audio. - ambient, - - /// The category for playing recorded music or other sounds - /// that are central to the successful use of your app. - /// - /// When using this category, your app audio continues - /// with the Silent switch set to silent or when the screen locks. - /// - /// By default, using this category implies that your app’s audio - /// is nonmixable—activating your session will interrupt - /// any other audio sessions which are also nonmixable. - /// To allow mixing for this category, use the - /// [IosTextToSpeechAudioCategoryOptions.mixWithOthers] option. - playback, - - /// The category for recording (input) and playback (output) of audio, - /// such as for a Voice over Internet Protocol (VoIP) app. - /// - /// Your audio continues with the Silent switch set to silent and with the screen locked. - /// This category is appropriate for simultaneous recording and playback, - /// and also for apps that record and play back, but not simultaneously. - playAndRecord, -} - -/// Audio session mode identifiers for iOS. -/// -/// See also: -/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/mode -enum IosTextToSpeechAudioMode { - /// The default audio session mode. - /// - /// You can use this mode with every [IosTextToSpeechAudioCategory]. - defaultMode, - - /// A mode that the GameKit framework sets on behalf of an application - /// that uses GameKit’s voice chat service. - /// - /// This mode is valid only with the - /// [IosTextToSpeechAudioCategory.playAndRecord] category. - /// - /// Don’t set this mode directly. If you need similar behavior and aren’t - /// using a `GKVoiceChat` object, use [voiceChat] or [videoChat] instead. - gameChat, - - /// A mode that indicates that your app is performing measurement of audio input or output. - /// - /// Use this mode for apps that need to minimize the amount of - /// system-supplied signal processing to input and output signals. - /// If recording on devices with more than one built-in microphone, - /// the session uses the primary microphone. - /// - /// For use with the [IosTextToSpeechAudioCategory.playback] or - /// [IosTextToSpeechAudioCategory.playAndRecord] category. - /// - /// **Important:** This mode disables some dynamics processing on input and output signals, - /// resulting in a lower-output playback level. - measurement, - - /// A mode that indicates that your app is playing back movie content. - /// - /// When you set this mode, the audio session uses signal processing to enhance - /// movie playback for certain audio routes such as built-in speaker or headphones. - /// You may only use this mode with the - /// [IosTextToSpeechAudioCategory.playback] category. - moviePlayback, - - /// A mode used for continuous spoken audio to pause the audio when another app plays a short audio prompt. - /// - /// This mode is appropriate for apps that play continuous spoken audio, - /// such as podcasts or audio books. Setting this mode indicates that your app - /// should pause, rather than duck, its audio if another app plays - /// a spoken audio prompt. After the interrupting app’s audio ends, you can - /// resume your app’s audio playback. - spokenAudio, - - /// A mode that indicates that your app is engaging in online video conferencing. - /// - /// Use this mode for video chat apps that use the - /// [IosTextToSpeechAudioCategory.playAndRecord] category. - /// When you set this mode, the audio session optimizes the device’s tonal - /// equalization for voice. It also reduces the set of allowable audio routes - /// to only those appropriate for video chat. - /// - /// Using this mode has the side effect of enabling the - /// [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. - videoChat, - - /// A mode that indicates that your app is recording a movie. - /// - /// This mode is valid only with the - /// [IosTextToSpeechAudioCategory.playAndRecord] category. - /// On devices with more than one built-in microphone, - /// the audio session uses the microphone closest to the video camera. - /// - /// Use this mode to ensure that the system provides appropriate audio-signal processing. - videoRecording, - - /// A mode that indicates that your app is performing two-way voice communication, - /// such as using Voice over Internet Protocol (VoIP). - /// - /// Use this mode for Voice over IP (VoIP) apps that use the - /// [IosTextToSpeechAudioCategory.playAndRecord] category. - /// When you set this mode, the session optimizes the device’s tonal - /// equalization for voice and reduces the set of allowable audio routes - /// to only those appropriate for voice chat. - /// - /// Using this mode has the side effect of enabling the - /// [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. - voiceChat, - - /// A mode that indicates that your app plays audio using text-to-speech. - /// - /// Setting this mode allows for different routing behaviors when your app - /// is connected to certain audio devices, such as CarPlay. - /// An example of an app that uses this mode is a turn-by-turn navigation app - /// that plays short prompts to the user. - /// - /// Typically, apps of the same type also configure their sessions to use the - /// [IosTextToSpeechAudioCategoryOptions.duckOthers] and - /// [IosTextToSpeechAudioCategoryOptions.interruptSpokenAudioAndMixWithOthers] options. - voicePrompt, -} - -/// Audio session category options for iOS. -/// -/// See also: -/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions -enum IosTextToSpeechAudioCategoryOptions { - /// An option that indicates whether audio from this session mixes with audio - /// from active sessions in other audio apps. - /// - /// You can set this option explicitly only if the audio session category - /// is [IosTextToSpeechAudioCategory.playAndRecord] or - /// [IosTextToSpeechAudioCategory.playback]. - /// If you set the audio session category to [IosTextToSpeechAudioCategory.ambient], - /// the session automatically sets this option. - /// Likewise, setting the [duckOthers] or [interruptSpokenAudioAndMixWithOthers] - /// options also enables this option. - /// - /// If you set this option, your app mixes its audio with audio playing - /// in background apps, such as the Music app. - mixWithOthers, - - /// An option that reduces the volume of other audio sessions while audio - /// from this session plays. - /// - /// You can set this option only if the audio session category is - /// [IosTextToSpeechAudioCategory.playAndRecord] or - /// [IosTextToSpeechAudioCategory.playback]. - /// Setting it implicitly sets the [mixWithOthers] option. - /// - /// Use this option to mix your app’s audio with that of others. - /// While your app plays its audio, the system reduces the volume of other - /// audio sessions to make yours more prominent. If your app provides - /// occasional spoken audio, such as in a turn-by-turn navigation app - /// or an exercise app, you should also set the [interruptSpokenAudioAndMixWithOthers] option. - /// - /// Note that ducking begins when you activate your app’s audio session - /// and ends when you deactivate the session. - /// - /// See also: - /// * [FlutterTts.setSharedInstance] - duckOthers, - - /// An option that determines whether to pause spoken audio content - /// from other sessions when your app plays its audio. - /// - /// You can set this option only if the audio session category is - /// [IosTextToSpeechAudioCategory.playAndRecord] or - /// [IosTextToSpeechAudioCategory.playback]. - /// Setting this option also sets [mixWithOthers]. - /// - /// If you set this option, the system mixes your audio with other - /// audio sessions, but interrupts (and stops) audio sessions that use the - /// [IosTextToSpeechAudioMode.spokenAudio] audio session mode. - /// It pauses the audio from other apps as long as your session is active. - /// After your audio session deactivates, the system resumes the interrupted app’s audio. - /// - /// Set this option if your app’s audio is occasional and spoken, - /// such as in a turn-by-turn navigation app or an exercise app. - /// This avoids intelligibility problems when two spoken audio apps mix. - /// If you set this option, also set the [duckOthers] option unless - /// you have a specific reason not to. Ducking other audio, rather than - /// interrupting it, is appropriate when the other audio isn’t spoken audio. - interruptSpokenAudioAndMixWithOthers, - - /// An option that determines whether Bluetooth hands-free devices appear - /// as available input routes. - /// - /// You can set this option only if the audio session category is - /// [IosTextToSpeechAudioCategory.playAndRecord] or - /// [IosTextToSpeechAudioCategory.playback]. - /// - /// You’re required to set this option to allow routing audio input and output - /// to a paired Bluetooth Hands-Free Profile (HFP) device. - /// If you clear this option, paired Bluetooth HFP devices don’t show up - /// as available audio input routes. - allowBluetooth, - - /// An option that determines whether you can stream audio from this session - /// to Bluetooth devices that support the Advanced Audio Distribution Profile (A2DP). - /// - /// A2DP is a stereo, output-only profile intended for higher bandwidth - /// audio use cases, such as music playback. - /// The system automatically routes to A2DP ports if you configure an - /// app’s audio session to use the [IosTextToSpeechAudioCategory.ambient], - /// [IosTextToSpeechAudioCategory.ambientSolo], or - /// [IosTextToSpeechAudioCategory.playback] categories. - /// - /// Starting with iOS 10.0, apps using the - /// [IosTextToSpeechAudioCategory.playAndRecord] category may also allow - /// routing output to paired Bluetooth A2DP devices. To enable this behavior, - /// pass this category option when setting your audio session’s category. - /// - /// Note: If this option and the [allowBluetooth] option are both set, - /// when a single device supports both the Hands-Free Profile (HFP) and A2DP, - /// the system gives hands-free ports a higher priority for routing. - allowBluetoothA2DP, - - /// An option that determines whether you can stream audio - /// from this session to AirPlay devices. - /// - /// Setting this option enables the audio session to route audio output - /// to AirPlay devices. You can only explicitly set this option if the - /// audio session’s category is set to [IosTextToSpeechAudioCategory.playAndRecord]. - /// For most other audio session categories, the system sets this option implicitly. - allowAirPlay, - - /// An option that determines whether audio from the session defaults to the built-in speaker instead of the receiver. - /// - /// You can set this option only when using the - /// [IosTextToSpeechAudioCategory.playAndRecord] category. - /// It’s used to modify the category’s routing behavior so that audio - /// is always routed to the speaker rather than the receiver if - /// no other accessories, such as headphones, are in use. - /// - /// When using this option, the system honors user gestures. - /// For example, plugging in a headset causes the route to change to - /// headset mic/headphones, and unplugging the headset causes the route - /// to change to built-in mic/speaker (as opposed to built-in mic/receiver) - /// when you’ve set this override. - /// - /// In the case of using a USB input-only accessory, audio input - /// comes from the accessory, and the system routes audio to the headphones, - /// if attached, or to the speaker if the headphones aren’t plugged in. - /// The use case is to route audio to the speaker instead of the receiver - /// in cases where the audio would normally go to the receiver. - defaultToSpeaker, -} - -class SpeechRateValidRange { - final double min; - final double normal; - final double max; - final TextToSpeechPlatform platform; - - SpeechRateValidRange(this.min, this.normal, this.max, this.platform); -} - -// Provides Platform specific TTS services (Android: TextToSpeech, IOS: AVSpeechSynthesizer) -class FlutterTts { - static const MethodChannel _channel = MethodChannel('flutter_tts'); - - VoidCallback? startHandler; - VoidCallback? completionHandler; - VoidCallback? pauseHandler; - VoidCallback? continueHandler; - VoidCallback? cancelHandler; - ProgressHandler? progressHandler; - ErrorHandler? errorHandler; - - FlutterTts() { - _channel.setMethodCallHandler(platformCallHandler); - } - - /// [Future] which sets speak's future to return on completion of the utterance - Future awaitSpeakCompletion(bool awaitCompletion) async => - await _channel.invokeMethod('awaitSpeakCompletion', awaitCompletion); - - /// [Future] which sets synthesize to file's future to return on completion of the synthesize - /// ***Android, iOS, and macOS supported only*** - Future awaitSynthCompletion(bool awaitCompletion) async => - await _channel.invokeMethod('awaitSynthCompletion', awaitCompletion); - - /// [Future] which invokes the platform specific method for speaking - Future speak(String text, {bool focus = false}) async { - if (!kIsWeb && Platform.isAndroid) { - return await _channel.invokeMethod('speak', { - "text": text, - "focus": focus, - }); - } else { - return await _channel.invokeMethod('speak', text); - } - } - - /// [Future] which invokes the platform specific method for pause - Future pause() async => await _channel.invokeMethod('pause'); - - /// [Future] which invokes the platform specific method for getMaxSpeechInputLength - /// ***Android supported only*** - Future get getMaxSpeechInputLength async { - return await _channel.invokeMethod('getMaxSpeechInputLength'); - } - - /// [Future] which invokes the platform specific method for synthesizeToFile - /// ***Android and iOS supported only*** - Future synthesizeToFile(String text, String fileName, - [bool isFullPath = false]) async => - _channel.invokeMethod('synthesizeToFile', { - "text": text, - "fileName": fileName, - "isFullPath": isFullPath, - }); - - /// [Future] which invokes the platform specific method for setLanguage - Future setLanguage(String language) async => - await _channel.invokeMethod('setLanguage', language); - - /// [Future] which invokes the platform specific method for setSpeechRate - /// Allowed values are in the range from 0.0 (slowest) to 1.0 (fastest) - Future setSpeechRate(double rate) async => - await _channel.invokeMethod('setSpeechRate', rate); - - /// [Future] which invokes the platform specific method for setVolume - /// Allowed values are in the range from 0.0 (silent) to 1.0 (loudest) - Future setVolume(double volume) async => - await _channel.invokeMethod('setVolume', volume); - - /// [Future] which invokes the platform specific method for shared instance - /// ***iOS supported only*** - Future setSharedInstance(bool sharedSession) async => - await _channel.invokeMethod('setSharedInstance', sharedSession); - - /// [Future] which invokes the platform specific method for setting the autoStopSharedSession - /// default value is true - /// *** iOS, and macOS supported only*** - Future autoStopSharedSession(bool autoStop) async => - await _channel.invokeMethod('autoStopSharedSession', autoStop); - - /// [Future] which invokes the platform specific method for setting audio category - /// ***Ios supported only*** - Future setIosAudioCategory(IosTextToSpeechAudioCategory category, - List options, - [IosTextToSpeechAudioMode mode = - IosTextToSpeechAudioMode.defaultMode]) async { - const categoryToString = { - IosTextToSpeechAudioCategory.ambientSolo: iosAudioCategoryAmbientSolo, - IosTextToSpeechAudioCategory.ambient: iosAudioCategoryAmbient, - IosTextToSpeechAudioCategory.playback: iosAudioCategoryPlayback - }; - const optionsToString = { - IosTextToSpeechAudioCategoryOptions.mixWithOthers: - 'iosAudioCategoryOptionsMixWithOthers', - IosTextToSpeechAudioCategoryOptions.duckOthers: - 'iosAudioCategoryOptionsDuckOthers', - IosTextToSpeechAudioCategoryOptions.interruptSpokenAudioAndMixWithOthers: - 'iosAudioCategoryOptionsInterruptSpokenAudioAndMixWithOthers', - IosTextToSpeechAudioCategoryOptions.allowBluetooth: - 'iosAudioCategoryOptionsAllowBluetooth', - IosTextToSpeechAudioCategoryOptions.allowBluetoothA2DP: - 'iosAudioCategoryOptionsAllowBluetoothA2DP', - IosTextToSpeechAudioCategoryOptions.allowAirPlay: - 'iosAudioCategoryOptionsAllowAirPlay', - IosTextToSpeechAudioCategoryOptions.defaultToSpeaker: - 'iosAudioCategoryOptionsDefaultToSpeaker', - }; - const modeToString = { - IosTextToSpeechAudioMode.defaultMode: iosAudioModeDefault, - IosTextToSpeechAudioMode.gameChat: iosAudioModeGameChat, - IosTextToSpeechAudioMode.measurement: iosAudioModeMeasurement, - IosTextToSpeechAudioMode.moviePlayback: iosAudioModeMoviePlayback, - IosTextToSpeechAudioMode.spokenAudio: iosAudioModeSpokenAudio, - IosTextToSpeechAudioMode.videoChat: iosAudioModeVideoChat, - IosTextToSpeechAudioMode.videoRecording: iosAudioModeVideoRecording, - IosTextToSpeechAudioMode.voiceChat: iosAudioModeVoiceChat, - IosTextToSpeechAudioMode.voicePrompt: iosAudioModeVoicePrompt, - }; - if (!Platform.isIOS) return; - try { - return await _channel - .invokeMethod('setIosAudioCategory', { - iosAudioCategoryKey: categoryToString[category], - iosAudioCategoryOptionsKey: - options.map((o) => optionsToString[o]).toList(), - iosAudioModeKey: modeToString[mode], - }); - } on PlatformException catch (e) { - print( - 'setIosAudioCategory error, category: $category, mode: $mode, error: ${e.message}'); - } - } - - /// [Future] which invokes the platform specific method for setEngine - /// ***Android supported only*** - Future setEngine(String engine) async { - await _channel.invokeMethod('setEngine', engine); - } - - /// [Future] which invokes the platform specific method for setPitch - /// 1.0 is default and ranges from .5 to 2.0 - Future setPitch(double pitch) async => - await _channel.invokeMethod('setPitch', pitch); - - /// [Future] which invokes the platform specific method for setVoice - /// ***Android, iOS, and macOS supported only*** - Future setVoice(Map voice) async => - await _channel.invokeMethod('setVoice', voice); - - /// [Future] which resets the platform voice to the default - Future clearVoice() async => - await _channel.invokeMethod('clearVoice'); - - /// [Future] which invokes the platform specific method for stop - Future stop() async => await _channel.invokeMethod('stop'); - - /// [Future] which invokes the platform specific method for getLanguages - /// Android issues with API 21 & 22 - /// Returns a list of available languages - Future get getLanguages async { - final languages = await _channel.invokeMethod('getLanguages'); - return languages; - } - - /// [Future] which invokes the platform specific method for getEngines - /// Returns a list of installed TTS engines - /// ***Android supported only*** - Future get getEngines async { - final engines = await _channel.invokeMethod('getEngines'); - return engines; - } - - /// [Future] which invokes the platform specific method for getDefaultEngine - /// Returns a `String` of the default engine name - /// ***Android supported only *** - Future get getDefaultEngine async { - final engineName = await _channel.invokeMethod('getDefaultEngine'); - return engineName; - } - - /// [Future] which invokes the platform specific method for getDefaultVoice - /// Returns a `Map` containing a voice name and locale - /// ***Android supported only *** - Future get getDefaultVoice async { - final voice = await _channel.invokeMethod('getDefaultVoice'); - return voice; - } - - /// [Future] which invokes the platform specific method for getVoices - /// Returns a `List` of `Maps` containing a voice name and locale - /// For iOS specifically, it also includes quality, gender, and identifier - /// ***Android, iOS, and macOS supported only*** - Future get getVoices async { - final voices = await _channel.invokeMethod('getVoices'); - return voices; - } - - /// [Future] which invokes the platform specific method for isLanguageAvailable - /// Returns `true` or `false` - Future isLanguageAvailable(String language) async => - await _channel.invokeMethod('isLanguageAvailable', language); - - /// [Future] which invokes the platform specific method for isLanguageInstalled - /// Returns `true` or `false` - /// ***Android supported only*** - Future isLanguageInstalled(String language) async => - await _channel.invokeMethod('isLanguageInstalled', language); - - /// [Future] which invokes the platform specific method for areLanguagesInstalled - /// Returns a HashMap with `true` or `false` for each submitted language. - /// ***Android supported only*** - Future areLanguagesInstalled(List languages) async => - await _channel.invokeMethod('areLanguagesInstalled', languages); - - Future get getSpeechRateValidRange async { - final validRange = await _channel.invokeMethod('getSpeechRateValidRange') - as Map; - final min = double.parse(validRange['min'].toString()); - final normal = double.parse(validRange['normal'].toString()); - final max = double.parse(validRange['max'].toString()); - final platformStr = validRange['platform'].toString(); - final platform = - TextToSpeechPlatform.values.firstWhere((e) => e.name == platformStr); - - return SpeechRateValidRange(min, normal, max, platform); - } - - /// [Future] which invokes the platform specific method for setSilence - /// 0 means start the utterance immediately. If the value is greater than zero a silence period in milliseconds is set according to the parameter - /// ***Android supported only*** - Future setSilence(int timems) async => - await _channel.invokeMethod('setSilence', timems); - - /// [Future] which invokes the platform specific method for setQueueMode - /// 0 means QUEUE_FLUSH - Queue mode where all entries in the playback queue (media to be played and text to be synthesized) are dropped and replaced by the new entry. - /// Queues are flushed with respect to a given calling app. Entries in the queue from other calls are not discarded. - /// 1 means QUEUE_ADD - Queue mode where the new entry is added at the end of the playback queue. - /// ***Android supported only*** - Future setQueueMode(int queueMode) async => - await _channel.invokeMethod('setQueueMode', queueMode); - - void setStartHandler(VoidCallback callback) { - startHandler = callback; - } - - void setCompletionHandler(VoidCallback callback) { - completionHandler = callback; - } - - void setContinueHandler(VoidCallback callback) { - continueHandler = callback; - } - - void setPauseHandler(VoidCallback callback) { - pauseHandler = callback; - } - - void setCancelHandler(VoidCallback callback) { - cancelHandler = callback; - } - - void setProgressHandler(ProgressHandler callback) { - progressHandler = callback; - } - - void setErrorHandler(ErrorHandler handler) { - errorHandler = handler; - } - - /// Platform listeners - Future platformCallHandler(MethodCall call) async { - switch (call.method) { - case "speak.onStart": - if (startHandler != null) { - startHandler!(); - } - break; - - case "synth.onStart": - if (startHandler != null) { - startHandler!(); - } - break; - case "speak.onComplete": - if (completionHandler != null) { - completionHandler!(); - } - break; - case "synth.onComplete": - if (completionHandler != null) { - completionHandler!(); - } - break; - case "speak.onPause": - if (pauseHandler != null) { - pauseHandler!(); - } - break; - case "speak.onContinue": - if (continueHandler != null) { - continueHandler!(); - } - break; - case "speak.onCancel": - if (cancelHandler != null) { - cancelHandler!(); - } - break; - case "speak.onError": - if (errorHandler != null) { - errorHandler!(call.arguments); - } - break; - case 'speak.onProgress': - if (progressHandler != null) { - final args = call.arguments as Map; - progressHandler!( - args['text'].toString(), - int.parse(args['start'].toString()), - int.parse(args['end'].toString()), - args['word'].toString(), - ); - } - break; - case "synth.onError": - if (errorHandler != null) { - errorHandler!(call.arguments); - } - break; - default: - print('Unknown method ${call.method}'); - } - } - - Future setAudioAttributesForNavigation() async { - await _channel.invokeMethod('setAudioAttributesForNavigation'); - } -} +export 'src/flutter_tts.dart'; +export 'src/flutter_tts_native.dart' + if (dart.library.html) 'src/flutter_tts_web.dart'; +export 'src/flutter_tts_platform_interface.dart'; +export 'src/messages.g.dart'; diff --git a/lib/src/flutter_tts.dart b/lib/src/flutter_tts.dart new file mode 100644 index 00000000..f397db40 --- /dev/null +++ b/lib/src/flutter_tts.dart @@ -0,0 +1,5 @@ +import 'package:flutter_tts/src/flutter_tts_platform_interface.dart'; + +class FlutterTts { + static FlutterTtsPlatform get platform => FlutterTtsPlatform.instance; +} diff --git a/lib/src/flutter_tts_android.dart b/lib/src/flutter_tts_android.dart new file mode 100644 index 00000000..ad6a773a --- /dev/null +++ b/lib/src/flutter_tts_android.dart @@ -0,0 +1,180 @@ +import 'package:flutter_tts/src/flutter_tts_method_channel.dart'; +import 'package:flutter_tts/src/flutter_tts_platform_interface.dart'; +import 'package:flutter_tts/src/messages.g.dart'; + +class FlutterTtsAndroid extends FlutterTtsMethodChannel { + static void registerWith() { + FlutterTtsPlatform.instance = FlutterTtsAndroid(); + } + + final androidHostApi = AndroidTtsHostApi(); + + /// [Future] which sets synthesize to file's future to return on completion of the synthesize + /// ***Android, iOS, and macOS supported only*** + Future> awaitSynthCompletion( + bool awaitCompletion, + ) async { + try { + return ResultDart.success( + await androidHostApi.awaitSynthCompletion(awaitCompletion), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for getMaxSpeechInputLength + /// ***Android supported only*** + Future> getMaxSpeechInputLength() async { + try { + return ResultDart.success(await androidHostApi.getMaxSpeechInputLength()); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for setEngine + /// ***Android supported only*** + Future> setEngine(String engine) async { + try { + return ResultDart.success(await androidHostApi.setEngine(engine)); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for getEngines + /// Returns a list of installed TTS engines + /// ***Android supported only*** + Future>> getEngines() async { + try { + return ResultDart.success(await androidHostApi.getEngines()); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for getDefaultEngine + /// Returns a `String` of the default engine name + /// ***Android supported only *** + Future> getDefaultEngine() async { + try { + return ResultDart.success(await androidHostApi.getDefaultEngine()); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for getDefaultVoice + /// Returns a `Map` containing a voice name and locale + /// ***Android supported only *** + Future> getDefaultVoice() async { + try { + return ResultDart.success(await androidHostApi.getDefaultVoice()); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for synthesizeToFile + Future> synthesizeToFile( + String text, + String fileName, [ + bool isFullPath = false, + ]) async { + try { + return ResultDart.success( + await androidHostApi.synthesizeToFile(text, fileName, isFullPath), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for isLanguageInstalled + /// Returns `true` or `false` + /// ***Android supported only*** + Future> isLanguageInstalled(String language) async { + try { + return ResultDart.success( + await androidHostApi.isLanguageInstalled(language), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for isLanguageAvailable + /// Returns `true` or `false` + Future> isLanguageAvailable(String language) async { + try { + return ResultDart.success( + await androidHostApi.isLanguageAvailable(language), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for areLanguagesInstalled + /// Returns a HashMap with `true` or `false` for each submitted language. + /// ***Android supported only*** + Future>> areLanguagesInstalled( + List languages, + ) async { + try { + return ResultDart.success( + await androidHostApi.areLanguagesInstalled(languages), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for getSpeechRateValidRange + /// Returns a `SpeechRateValidRange` object containing the minimum, normal, and maximum speech rate values for the current platform. + /// ***Android supported only*** + Future> getSpeechRateValidRange() async { + try { + return ResultDart.success(await androidHostApi.getSpeechRateValidRange()); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for setSilence + /// 0 means start the utterance immediately. If the value is greater than zero a silence period in milliseconds is set according to the parameter + /// ***Android supported only*** + Future> setSilence(int timems) async { + try { + return ResultDart.success(await androidHostApi.setSilence(timems)); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for setQueueMode + /// 0 means QUEUE_FLUSH - Queue mode where all entries in the playback queue (media to be played and text to be synthesized) are dropped and replaced by the new entry. + /// Queues are flushed with respect to a given calling app. Entries in the queue from other calls are not discarded. + /// 1 means QUEUE_ADD - Queue mode where the new entry is added at the end of the playback queue. + /// ***Android supported only*** + Future> setQueueMode(int queueMode) async { + try { + return ResultDart.success(await androidHostApi.setQueueMode(queueMode)); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for setAudioAttributesForNavigation + /// ***Android supported only*** + Future> setAudioAttributesForNavigation() async { + try { + return ResultDart.success( + await androidHostApi.setAudioAttributesForNavigation(), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } +} diff --git a/lib/src/flutter_tts_ios.dart b/lib/src/flutter_tts_ios.dart new file mode 100644 index 00000000..b1be187d --- /dev/null +++ b/lib/src/flutter_tts_ios.dart @@ -0,0 +1,107 @@ +import 'package:flutter_tts/src/flutter_tts_method_channel.dart'; +import 'package:flutter_tts/src/flutter_tts_platform_interface.dart'; +import 'package:flutter_tts/src/messages.g.dart'; + +class FlutterTtsIos extends FlutterTtsMethodChannel { + static void registerWith() { + FlutterTtsPlatform.instance = FlutterTtsIos(); + } + + final IosTtsHostApi iosHostApi = IosTtsHostApi(); + + /// [Future] which sets synthesize to file's future to return on completion of the synthesize + /// ***Android, iOS, and macOS supported only*** + Future> awaitSynthCompletion( + bool awaitCompletion, + ) async { + try { + return ResultDart.success( + await iosHostApi.awaitSynthCompletion(awaitCompletion), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for synthesizeToFile + Future> synthesizeToFile( + String text, + String fileName, [ + bool isFullPath = false, + ]) async { + try { + return ResultDart.success( + await iosHostApi.synthesizeToFile(text, fileName, isFullPath), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for shared instance + /// ***iOS supported only*** + Future> setSharedInstance(bool sharedSession) async { + try { + return ResultDart.success( + await iosHostApi.setSharedInstance(sharedSession), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for setting the autoStopSharedSession + /// default value is true + /// *** iOS, and macOS supported only*** + Future> autoStopSharedSession(bool autoStop) async { + try { + return ResultDart.success( + await iosHostApi.autoStopSharedSession(autoStop), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for setting audio category + /// ***Ios supported only*** + Future> setIosAudioCategory( + IosTextToSpeechAudioCategory category, + List options, { + IosTextToSpeechAudioMode mode = IosTextToSpeechAudioMode.defaultMode, + }) async { + try { + return ResultDart.success( + await iosHostApi.setIosAudioCategory(category, options, mode: mode), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + Future> getSpeechRateValidRange() async { + try { + return ResultDart.success(await iosHostApi.getSpeechRateValidRange()); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for isLanguageAvailable + /// Returns `true` or `false` + Future> isLanguageAvailable(String language) async { + try { + return ResultDart.success(await iosHostApi.isLanguageAvailable(language)); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + Future> setLanguange(String language) async { + try { + return ResultDart.success(await iosHostApi.setLanguange(language)); + } on Exception catch (e) { + return ResultDart.error(e); + } + } +} diff --git a/lib/src/flutter_tts_macos.dart b/lib/src/flutter_tts_macos.dart new file mode 100644 index 00000000..3a859c70 --- /dev/null +++ b/lib/src/flutter_tts_macos.dart @@ -0,0 +1,53 @@ +import 'package:flutter_tts/src/flutter_tts_method_channel.dart'; +import 'package:flutter_tts/src/flutter_tts_platform_interface.dart'; +import 'package:flutter_tts/src/messages.g.dart'; + +class FlutterTtsMacos extends FlutterTtsMethodChannel { + static void registerWith() { + FlutterTtsPlatform.instance = FlutterTtsMacos(); + } + + final macosHostApi = MacosTtsHostApi(); + + /// [Future] which sets synthesize to file's future to return on completion of the synthesize + /// ***Android, iOS, and macOS supported only*** + Future> awaitSynthCompletion( + bool awaitCompletion, + ) async { + try { + return ResultDart.success( + await macosHostApi.awaitSynthCompletion(awaitCompletion), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for getting the speech rate valid range + /// ***iOS, and macOS supported only*** + Future> getSpeechRateValidRange() async { + try { + return ResultDart.success(await macosHostApi.getSpeechRateValidRange()); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + Future> setLanguange(String language) async { + try { + return ResultDart.success(await macosHostApi.setLanguange(language)); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + Future> isLanguageAvailable(String language) async { + try { + return ResultDart.success( + await macosHostApi.isLanguageAvailable(language), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } +} diff --git a/lib/src/flutter_tts_method_channel.dart b/lib/src/flutter_tts_method_channel.dart new file mode 100644 index 00000000..90a1f665 --- /dev/null +++ b/lib/src/flutter_tts_method_channel.dart @@ -0,0 +1,167 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_tts/src/flutter_tts_platform_interface.dart'; +import 'package:flutter_tts/src/messages.g.dart'; +import 'package:multiple_result/multiple_result.dart'; + +class FlutterTtsMethodChannel extends FlutterTtsPlatform + implements TtsFlutterApi { + @protected + final TtsHostApi hostApi = TtsHostApi(); + + FlutterTtsMethodChannel() { + TtsFlutterApi.setUp(this); + } + + @override + Future> awaitSpeakCompletion( + bool awaitCompletion, + ) async { + try { + return Result.success( + await hostApi.awaitSpeakCompletion(awaitCompletion), + ); + } on Exception catch (e) { + return Result.error(e); + } + } + + @override + Future> clearVoice() async { + try { + return Result.success(await hostApi.clearVoice()); + } on Exception catch (e) { + return Result.error(e); + } + } + + @override + Future>> getLanguages() async { + try { + return Result.success(await hostApi.getLanguages()); + } on Exception catch (e) { + return Result.error(e); + } + } + + @override + Future>> getVoices() async { + try { + return Result.success(await hostApi.getVoices()); + } on Exception catch (e) { + return Result.error(e); + } + } + + @override + Future> pause() async { + try { + return Result.success(await hostApi.pause()); + } on Exception catch (e) { + return Result.error(e); + } + } + + @override + Future> setPitch(double pitch) async { + try { + return Result.success(await hostApi.setPitch(pitch)); + } on Exception catch (e) { + return Result.error(e); + } + } + + @override + Future> setSpeechRate(double rate) async { + try { + return Result.success(await hostApi.setSpeechRate(rate)); + } on Exception catch (e) { + return Result.error(e); + } + } + + @override + Future> setVoice(Voice voice) async { + try { + return Result.success(await hostApi.setVoice(voice)); + } on Exception catch (e) { + return Result.error(e); + } + } + + @override + Future> setVolume(double volume) async { + try { + return Result.success(await hostApi.setVolume(volume)); + } on Exception catch (e) { + return Result.error(e); + } + } + + @override + Future> speak(String text, {bool focus = false}) async { + try { + return Result.success(await hostApi.speak(text, focus)); + } on Exception catch (e) { + return Result.error(e); + } + } + + @override + Future> stop() async { + try { + return Result.success(await hostApi.stop()); + } on Exception catch (e) { + return Result.error(e); + } + } + + @override + void onSpeakCancelCb() { + onSpeakCancel?.call(); + } + + @override + void onSpeakCompleteCb() { + onSpeakComplete?.call(); + } + + @override + void onSpeakResumeCb() { + onSpeakResume?.call(); + } + + @override + void onSpeakErrorCb(String error) { + onSpeakError?.call(error); + } + + @override + void onSpeakPauseCb() { + onSpeakPause?.call(); + } + + @override + void onSpeakProgressCb(TtsProgress progress) { + onSpeakProgress?.call(progress); + } + + @override + void onSpeakStartCb() { + onSpeakStart?.call(); + } + + @override + void onSynthCompleteCb() { + onSynthComplete?.call(); + } + + @override + void onSynthErrorCb(String error) { + onSynthError?.call(error); + } + + @override + void onSynthStartCb() { + onSynthStart?.call(); + } +} diff --git a/lib/src/flutter_tts_native.dart b/lib/src/flutter_tts_native.dart new file mode 100644 index 00000000..599d51a5 --- /dev/null +++ b/lib/src/flutter_tts_native.dart @@ -0,0 +1,4 @@ +export 'package:flutter_tts/src/flutter_tts_android.dart'; +export 'package:flutter_tts/src/flutter_tts_ios.dart'; +export 'package:flutter_tts/src/flutter_tts_macos.dart'; +export 'package:flutter_tts/src/flutter_tts_method_channel.dart'; diff --git a/lib/src/flutter_tts_platform_interface.dart b/lib/src/flutter_tts_platform_interface.dart new file mode 100644 index 00000000..86316d30 --- /dev/null +++ b/lib/src/flutter_tts_platform_interface.dart @@ -0,0 +1,81 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_tts/src/flutter_tts_method_channel.dart'; +import 'package:flutter_tts/src/messages.g.dart'; +import 'package:multiple_result/multiple_result.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +typedef ResultDart = Result; +typedef SuccessDart = Success; +typedef FailureDart = Error; + +abstract class FlutterTtsPlatform extends PlatformInterface { + static const token = Object(); + + static FlutterTtsPlatform _instance = FlutterTtsMethodChannel(); + + /// The default instance of [FlutterTtsPlatform ] to use. + /// + /// Defaults to [MethodChannelFlutterSysFonts]. + static FlutterTtsPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [FlutterTtsPlatform ] when + /// they register themselves. + static set instance(FlutterTtsPlatform instance) { + PlatformInterface.verifyToken(instance, FlutterTtsPlatform.token); + _instance = instance; + } + + VoidCallback? onSpeakStart; + VoidCallback? onSpeakComplete; + VoidCallback? onSpeakPause; + VoidCallback? onSpeakResume; + VoidCallback? onSpeakCancel; + ValueChanged? onSpeakError; + ValueChanged? onSpeakProgress; + + VoidCallback? onSynthStart; + VoidCallback? onSynthComplete; + ValueChanged? onSynthError; + + FlutterTtsPlatform() : super(token: token); + + /// [Future] which sets speak's future to return on completion of the utterance + Future> awaitSpeakCompletion(bool awaitCompletion); + + /// [Future] which invokes the platform specific method for speaking + Future> speak(String text, {bool focus = false}); + + /// [Future] which invokes the platform specific method for pause + Future> pause(); + + /// [Future] which invokes the platform specific method for stop + Future> stop(); + + /// [Future] which invokes the platform specific method for setSpeechRate + /// Allowed values are in the range from 0.0 (slowest) to 1.0 (fastest) + Future> setSpeechRate(double rate); + + /// [Future] which invokes the platform specific method for setVolume + /// Allowed values are in the range from 0.0 (silent) to 1.0 (loudest) + Future> setVolume(double volume); + + /// [Future] which invokes the platform specific method for setPitch + /// 1.0 is default and ranges from .5 to 2.0 + Future> setPitch(double pitch); + + Future>> getLanguages(); + + /// [Future] which invokes the platform specific method for getVoices + /// Returns a `List` of `Maps` containing a voice name and locale + /// For iOS specifically, it also includes quality, gender, and identifier + /// ***Android, iOS, and macOS supported only*** + Future>> getVoices(); + + /// [Future] which invokes the platform specific method for setVoice + Future> setVoice(Voice voice); + + /// [Future] which resets the platform voice to the default + Future> clearVoice(); +} diff --git a/lib/flutter_tts_web.dart b/lib/src/flutter_tts_web.dart similarity index 52% rename from lib/flutter_tts_web.dart rename to lib/src/flutter_tts_web.dart index abff5ad3..b14ab046 100644 --- a/lib/flutter_tts_web.dart +++ b/lib/src/flutter_tts_web.dart @@ -1,23 +1,26 @@ import 'dart:async'; -import 'dart:collection'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; -import 'package:flutter/services.dart'; +import 'package:flutter_tts/src/flutter_tts_platform_interface.dart'; +import 'package:flutter_tts/src/flutter_tts_web_interop_types.dart'; +import 'package:flutter_tts/src/messages.g.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -import 'interop_types.dart'; +export 'package:flutter_tts/src/flutter_tts_web_interop_types.dart'; enum TtsState { playing, stopped, paused, continued } -class FlutterTtsPlugin { - static const String platformChannel = "flutter_tts"; - static late MethodChannel channel; - bool awaitSpeakCompletion = false; +class FlutterTtsWeb extends FlutterTtsPlatform { + static void registerWith(Registrar registrar) { + FlutterTtsPlatform.instance = FlutterTtsWeb(); + } + + bool isAwaitSpeakCompletion = false; TtsState ttsState = TtsState.stopped; - Completer? _speechCompleter; + Completer? _speechCompleter; bool get isPlaying => ttsState == TtsState.playing; @@ -27,20 +30,13 @@ class FlutterTtsPlugin { bool get isContinued => ttsState == TtsState.continued; - static void registerWith(Registrar registrar) { - channel = - MethodChannel(platformChannel, const StandardMethodCodec(), registrar); - final instance = FlutterTtsPlugin(); - channel.setMethodCallHandler(instance.handleMethodCall); - } - late final SpeechSynthesisUtterance utterance; List voices = []; List languages = []; Timer? t; bool supported = false; - FlutterTtsPlugin() { + FlutterTtsWeb() { try { utterance = SpeechSynthesisUtterance(); _listeners(); @@ -50,10 +46,92 @@ class FlutterTtsPlugin { } } + @override + Future>> getVoices() async { + try { + return ResultDart.success(await _getVoices()); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + @override + Future> clearVoice() async { + return ResultDart.success(TtsResult(success: true)); + } + + @override + Future> pause() async { + _pause(); + return ResultDart.success(TtsResult(success: true)); + } + + @override + Future> setPitch(double pitch) async { + _setPitch(pitch); + return ResultDart.success(TtsResult(success: true)); + } + + @override + Future> setSpeechRate(double rate) async { + _setRate(rate); + return ResultDart.success(TtsResult(success: true)); + } + + @override + Future> setVoice(Voice voice) async { + _setVoice(voice); + return ResultDart.success(TtsResult(success: true)); + } + + @override + Future> setVolume(double volume) async { + _setVolume(volume); + return ResultDart.success(TtsResult(success: true)); + } + + @override + Future> speak(String text, {bool focus = false}) async { + _speak(text); + if (isAwaitSpeakCompletion) { + _speechCompleter = Completer(); + return ResultDart.success(await _speechCompleter!.future); + } + + return ResultDart.success(TtsResult(success: true)); + } + + @override + Future> stop() async { + _stop(); + return ResultDart.success(TtsResult(success: true)); + } + + @override + Future> awaitSpeakCompletion( + bool awaitCompletion, + ) async { + isAwaitSpeakCompletion = awaitCompletion; + return ResultDart.success(TtsResult(success: true)); + } + + @override + Future>> getLanguages() async { + try { + return ResultDart.success(_getLanguages() ?? []); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + Future isLanguageAvailable(String lang) async { + return _isLanguageAvailable(lang); + } + void _listeners() { utterance.onStart = (JSAny e) { ttsState = TtsState.playing; - channel.invokeMethod("speak.onStart", null); + onSpeakStart?.call(); var bLocal = (utterance.voice?.isLocalService ?? false); if (!bLocal) { t = Timer.periodic(Duration(seconds: 14), (t) { @@ -73,21 +151,21 @@ class FlutterTtsPlugin { utterance.onEnd = (JSAny e) { ttsState = TtsState.stopped; if (_speechCompleter != null) { - _speechCompleter?.complete(); + _speechCompleter?.complete(TtsResult(success: true)); _speechCompleter = null; } t?.cancel(); - channel.invokeMethod("speak.onComplete", null); + onSpeakComplete?.call(); }.toJS; utterance.onPause = (JSAny e) { ttsState = TtsState.paused; - channel.invokeMethod("speak.onPause", null); + onSpeakPause?.call(); }.toJS; utterance.onResume = (JSAny e) { ttsState = TtsState.continued; - channel.invokeMethod("speak.onContinue", null); + onSpeakResume?.call(); }.toJS; utterance.onError = (JSObject event) { @@ -97,7 +175,7 @@ class FlutterTtsPlugin { } t?.cancel(); print(event); // Log the entire event object to get more details - channel.invokeMethod("speak.onError", event["error"]); + onSpeakError?.call(event["error"].toString()); }.toJS; utterance.onBoundary = (JSObject event) { @@ -111,73 +189,16 @@ class FlutterTtsPlugin { endIndex++; } String word = text.substring(charIndex, endIndex); - Map progressArgs = { - 'text': text, - 'start': charIndex, - 'end': endIndex, - 'word': word - }; - channel.invokeMethod("speak.onProgress", progressArgs); + TtsProgress progress = TtsProgress( + text: text, + start: charIndex, + end: endIndex, + word: word, + ); + onSpeakProgress?.call(progress); }.toJS; } - Future handleMethodCall(MethodCall call) async { - if (!supported) return; - switch (call.method) { - case 'speak': - final text = call.arguments as String?; - if (awaitSpeakCompletion) { - _speechCompleter = Completer(); - } - _speak(text); - if (awaitSpeakCompletion) { - return _speechCompleter?.future; - } - break; - case 'awaitSpeakCompletion': - awaitSpeakCompletion = (call.arguments as bool?) ?? false; - return 1; - case 'stop': - _stop(); - return 1; - case 'pause': - _pause(); - return 1; - case 'setLanguage': - final language = call.arguments as String; - _setLanguage(language); - return 1; - case 'getLanguages': - return _getLanguages(); - case 'getVoices': - return getVoices(); - case 'setVoice': - final tmpVoiceMap = - Map.from(call.arguments as LinkedHashMap); - return _setVoice(tmpVoiceMap); - case 'setSpeechRate': - final rate = call.arguments as double; - _setRate(rate); - return 1; - case 'setVolume': - final volume = call.arguments as double; - _setVolume(volume); - return 1; - case 'setPitch': - final pitch = call.arguments as double; - _setPitch(pitch); - return 1; - case 'isLanguageAvailable': - final lang = call.arguments as String; - return _isLanguageAvailable(lang); - default: - throw PlatformException( - code: 'Unimplemented', - details: "The flutter_tts plugin for web doesn't implement " - "the method '${call.method}'"); - } - } - void _speak(String? text) { if (text == null || text.isEmpty) return; if (ttsState == TtsState.stopped || ttsState == TtsState.paused) { @@ -205,29 +226,21 @@ class FlutterTtsPlugin { void _setRate(double rate) => utterance.rate = rate; void _setVolume(double volume) => utterance.volume = volume; void _setPitch(double pitch) => utterance.pitch = pitch; - void _setLanguage(String language) { - var targetList = synth.getVoices().toDart.where((e) { - return e.lang.toLowerCase().startsWith(language.toLowerCase()); - }); - if (targetList.isNotEmpty) { - utterance.voice = targetList.first; - utterance.lang = targetList.first.lang; - } - } - void _setVoice(Map voice) { + void _setVoice(Voice voice) { var tmpVoices = synth.getVoices().toDart; var targetList = tmpVoices.where((e) { - return voice["name"] == e.name && voice["locale"] == e.lang; + return voice.name == e.name && voice.locale == e.lang; }); + if (targetList.isNotEmpty) { utterance.voice = targetList.first; } } bool _isLanguageAvailable(String? language) { - if (voices.isEmpty) _setVoices(); - if (languages.isEmpty) _setLanguages(); + if (voices.isEmpty) _updateVoices(); + if (languages.isEmpty) _updateLanguages(); for (var lang in languages) { if (!language!.contains('-')) { lang = lang.split('-').first; @@ -237,24 +250,24 @@ class FlutterTtsPlugin { return false; } - List? _getLanguages() { - if (voices.isEmpty) _setVoices(); - if (languages.isEmpty) _setLanguages(); + List? _getLanguages() { + if (voices.isEmpty) _updateVoices(); + if (languages.isEmpty) _updateLanguages(); return languages; } - void _setVoices() { - voices = synth.getVoices().toDart; + Future> _getVoices() async { + _updateVoices(); + return voices + .map((voice) => Voice(name: voice.name, locale: voice.lang)) + .toList(); } - Future>> getVoices() async { - var tmpVoices = synth.getVoices().toDart; - return tmpVoices - .map((voice) => {"name": voice.name, "locale": voice.lang}) - .toList(); + void _updateVoices() { + voices = synth.getVoices().toDart; } - void _setLanguages() { + void _updateLanguages() { var langs = {}; for (var v in voices) { langs.add(v.lang); diff --git a/lib/interop_types.dart b/lib/src/flutter_tts_web_interop_types.dart similarity index 100% rename from lib/interop_types.dart rename to lib/src/flutter_tts_web_interop_types.dart diff --git a/lib/src/messages.g.dart b/lib/src/messages.g.dart new file mode 100644 index 00000000..383a2dbc --- /dev/null +++ b/lib/src/messages.g.dart @@ -0,0 +1,1917 @@ +// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} +bool _deepEquals(Object? a, Object? b) { + if (a is List && b is List) { + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); + } + return a == b; +} + + +enum FlutterTtsErrorCode { + /// general error code for TTS engine not available. + ttsNotAvailable, + /// The TTS engine failed to initialize in n second. + /// 1 second is the default timeout. + /// e.g. Some Android custom ROMS may trim TTS service, + /// and third party TTS engine may fail to initialize due to battery optimization. + ttsInitTimeout, + /// not supported on current os version + notSupportedOSVersion, +} + +/// Audio session category identifiers for iOS. +/// +/// See also: +/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/category +enum IosTextToSpeechAudioCategory { + /// The default audio session category. + /// + /// Your audio is silenced by screen locking and by the Silent switch. + /// + /// By default, using this category implies that your app’s audio + /// is nonmixable—activating your session will interrupt + /// any other audio sessions which are also nonmixable. + /// To allow mixing, use the [ambient] category instead. + ambientSolo, + /// The category for an app in which sound playback is nonprimary — that is, + /// your app also works with the sound turned off. + /// + /// This category is also appropriate for “play-along” apps, + /// such as a virtual piano that a user plays while the Music app is playing. + /// When you use this category, audio from other apps mixes with your audio. + /// Screen locking and the Silent switch (on iPhone, the Ring/Silent switch) silence your audio. + ambient, + /// The category for playing recorded music or other sounds + /// that are central to the successful use of your app. + /// + /// When using this category, your app audio continues + /// with the Silent switch set to silent or when the screen locks. + /// + /// By default, using this category implies that your app’s audio + /// is nonmixable—activating your session will interrupt + /// any other audio sessions which are also nonmixable. + /// To allow mixing for this category, use the + /// [IosTextToSpeechAudioCategoryOptions.mixWithOthers] option. + playback, + /// The category for recording (input) and playback (output) of audio, + /// such as for a Voice over Internet Protocol (VoIP) app. + /// + /// Your audio continues with the Silent switch set to silent and with the screen locked. + /// This category is appropriate for simultaneous recording and playback, + /// and also for apps that record and play back, but not simultaneously. + playAndRecord, +} + +/// Audio session mode identifiers for iOS. +/// +/// See also: +/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/mode +enum IosTextToSpeechAudioMode { + /// The default audio session mode. + /// + /// You can use this mode with every [IosTextToSpeechAudioCategory]. + defaultMode, + /// A mode that the GameKit framework sets on behalf of an application + /// that uses GameKit’s voice chat service. + /// + /// This mode is valid only with the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// + /// Don’t set this mode directly. If you need similar behavior and aren’t + /// using a `GKVoiceChat` object, use [voiceChat] or [videoChat] instead. + gameChat, + /// A mode that indicates that your app is performing measurement of audio input or output. + /// + /// Use this mode for apps that need to minimize the amount of + /// system-supplied signal processing to input and output signals. + /// If recording on devices with more than one built-in microphone, + /// the session uses the primary microphone. + /// + /// For use with the [IosTextToSpeechAudioCategory.playback] or + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// + /// **Important:** This mode disables some dynamics processing on input and output signals, + /// resulting in a lower-output playback level. + measurement, + /// A mode that indicates that your app is playing back movie content. + /// + /// When you set this mode, the audio session uses signal processing to enhance + /// movie playback for certain audio routes such as built-in speaker or headphones. + /// You may only use this mode with the + /// [IosTextToSpeechAudioCategory.playback] category. + moviePlayback, + /// A mode used for continuous spoken audio to pause the audio when another app plays a short audio prompt. + /// + /// This mode is appropriate for apps that play continuous spoken audio, + /// such as podcasts or audio books. Setting this mode indicates that your app + /// should pause, rather than duck, its audio if another app plays + /// a spoken audio prompt. After the interrupting app’s audio ends, you can + /// resume your app’s audio playback. + spokenAudio, + /// A mode that indicates that your app is engaging in online video conferencing. + /// + /// Use this mode for video chat apps that use the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// When you set this mode, the audio session optimizes the device’s tonal + /// equalization for voice. It also reduces the set of allowable audio routes + /// to only those appropriate for video chat. + /// + /// Using this mode has the side effect of enabling the + /// [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. + videoChat, + /// A mode that indicates that your app is recording a movie. + /// + /// This mode is valid only with the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// On devices with more than one built-in microphone, + /// the audio session uses the microphone closest to the video camera. + /// + /// Use this mode to ensure that the system provides appropriate audio-signal processing. + videoRecording, + /// A mode that indicates that your app is performing two-way voice communication, + /// such as using Voice over Internet Protocol (VoIP). + /// + /// Use this mode for Voice over IP (VoIP) apps that use the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// When you set this mode, the session optimizes the device’s tonal + /// equalization for voice and reduces the set of allowable audio routes + /// to only those appropriate for voice chat. + /// + /// Using this mode has the side effect of enabling the + /// [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. + voiceChat, + /// A mode that indicates that your app plays audio using text-to-speech. + /// + /// Setting this mode allows for different routing behaviors when your app + /// is connected to certain audio devices, such as CarPlay. + /// An example of an app that uses this mode is a turn-by-turn navigation app + /// that plays short prompts to the user. + /// + /// Typically, apps of the same type also configure their sessions to use the + /// [IosTextToSpeechAudioCategoryOptions.duckOthers] and + /// [IosTextToSpeechAudioCategoryOptions.interruptSpokenAudioAndMixWithOthers] options. + voicePrompt, +} + +/// Audio session category options for iOS. +/// +/// See also: +/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions +enum IosTextToSpeechAudioCategoryOptions { + /// An option that indicates whether audio from this session mixes with audio + /// from active sessions in other audio apps. + /// + /// You can set this option explicitly only if the audio session category + /// is [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// If you set the audio session category to [IosTextToSpeechAudioCategory.ambient], + /// the session automatically sets this option. + /// Likewise, setting the [duckOthers] or [interruptSpokenAudioAndMixWithOthers] + /// options also enables this option. + /// + /// If you set this option, your app mixes its audio with audio playing + /// in background apps, such as the Music app. + mixWithOthers, + /// An option that reduces the volume of other audio sessions while audio + /// from this session plays. + /// + /// You can set this option only if the audio session category is + /// [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// Setting it implicitly sets the [mixWithOthers] option. + /// + /// Use this option to mix your app’s audio with that of others. + /// While your app plays its audio, the system reduces the volume of other + /// audio sessions to make yours more prominent. If your app provides + /// occasional spoken audio, such as in a turn-by-turn navigation app + /// or an exercise app, you should also set the [interruptSpokenAudioAndMixWithOthers] option. + /// + /// Note that ducking begins when you activate your app’s audio session + /// and ends when you deactivate the session. + /// + /// See also: + /// * [FlutterTts.setSharedInstance] + duckOthers, + /// An option that determines whether to pause spoken audio content + /// from other sessions when your app plays its audio. + /// + /// You can set this option only if the audio session category is + /// [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// Setting this option also sets [mixWithOthers]. + /// + /// If you set this option, the system mixes your audio with other + /// audio sessions, but interrupts (and stops) audio sessions that use the + /// [IosTextToSpeechAudioMode.spokenAudio] audio session mode. + /// It pauses the audio from other apps as long as your session is active. + /// After your audio session deactivates, the system resumes the interrupted app’s audio. + /// + /// Set this option if your app’s audio is occasional and spoken, + /// such as in a turn-by-turn navigation app or an exercise app. + /// This avoids intelligibility problems when two spoken audio apps mix. + /// If you set this option, also set the [duckOthers] option unless + /// you have a specific reason not to. Ducking other audio, rather than + /// interrupting it, is appropriate when the other audio isn’t spoken audio. + interruptSpokenAudioAndMixWithOthers, + /// An option that determines whether Bluetooth hands-free devices appear + /// as available input routes. + /// + /// You can set this option only if the audio session category is + /// [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// + /// You’re required to set this option to allow routing audio input and output + /// to a paired Bluetooth Hands-Free Profile (HFP) device. + /// If you clear this option, paired Bluetooth HFP devices don’t show up + /// as available audio input routes. + allowBluetooth, + /// An option that determines whether you can stream audio from this session + /// to Bluetooth devices that support the Advanced Audio Distribution Profile (A2DP). + /// + /// A2DP is a stereo, output-only profile intended for higher bandwidth + /// audio use cases, such as music playback. + /// The system automatically routes to A2DP ports if you configure an + /// app’s audio session to use the [IosTextToSpeechAudioCategory.ambient], + /// [IosTextToSpeechAudioCategory.ambientSolo], or + /// [IosTextToSpeechAudioCategory.playback] categories. + /// + /// Starting with iOS 10.0, apps using the + /// [IosTextToSpeechAudioCategory.playAndRecord] category may also allow + /// routing output to paired Bluetooth A2DP devices. To enable this behavior, + /// pass this category option when setting your audio session’s category. + /// + /// Note: If this option and the [allowBluetooth] option are both set, + /// when a single device supports both the Hands-Free Profile (HFP) and A2DP, + /// the system gives hands-free ports a higher priority for routing. + allowBluetoothA2DP, + /// An option that determines whether you can stream audio + /// from this session to AirPlay devices. + /// + /// Setting this option enables the audio session to route audio output + /// to AirPlay devices. You can only explicitly set this option if the + /// audio session’s category is set to [IosTextToSpeechAudioCategory.playAndRecord]. + /// For most other audio session categories, the system sets this option implicitly. + allowAirPlay, + /// An option that determines whether audio from the session defaults to the built-in speaker instead of the receiver. + /// + /// You can set this option only when using the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// It’s used to modify the category’s routing behavior so that audio + /// is always routed to the speaker rather than the receiver if + /// no other accessories, such as headphones, are in use. + /// + /// When using this option, the system honors user gestures. + /// For example, plugging in a headset causes the route to change to + /// headset mic/headphones, and unplugging the headset causes the route + /// to change to built-in mic/speaker (as opposed to built-in mic/receiver) + /// when you’ve set this override. + /// + /// In the case of using a USB input-only accessory, audio input + /// comes from the accessory, and the system routes audio to the headphones, + /// if attached, or to the speaker if the headphones aren’t plugged in. + /// The use case is to route audio to the speaker instead of the receiver + /// in cases where the audio would normally go to the receiver. + defaultToSpeaker, +} + +enum TtsPlatform { + android, + ios, +} + +class Voice { + Voice({ + required this.name, + required this.locale, + this.gender, + this.quality, + this.identifier, + }); + + String name; + + String locale; + + String? gender; + + String? quality; + + String? identifier; + + List _toList() { + return [ + name, + locale, + gender, + quality, + identifier, + ]; + } + + Object encode() { + return _toList(); } + + static Voice decode(Object result) { + result as List; + return Voice( + name: result[0]! as String, + locale: result[1]! as String, + gender: result[2] as String?, + quality: result[3] as String?, + identifier: result[4] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! Voice || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +class TtsResult { + TtsResult({ + required this.success, + this.message, + }); + + bool success; + + String? message; + + List _toList() { + return [ + success, + message, + ]; + } + + Object encode() { + return _toList(); } + + static TtsResult decode(Object result) { + result as List; + return TtsResult( + success: result[0]! as bool, + message: result[1] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! TtsResult || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +class TtsProgress { + TtsProgress({ + required this.text, + required this.start, + required this.end, + required this.word, + }); + + String text; + + int start; + + int end; + + String word; + + List _toList() { + return [ + text, + start, + end, + word, + ]; + } + + Object encode() { + return _toList(); } + + static TtsProgress decode(Object result) { + result as List; + return TtsProgress( + text: result[0]! as String, + start: result[1]! as int, + end: result[2]! as int, + word: result[3]! as String, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! TtsProgress || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +class TtsRateValidRange { + TtsRateValidRange({ + required this.minimum, + required this.normal, + required this.maximum, + required this.platform, + }); + + double minimum; + + double normal; + + double maximum; + + TtsPlatform platform; + + List _toList() { + return [ + minimum, + normal, + maximum, + platform, + ]; + } + + Object encode() { + return _toList(); } + + static TtsRateValidRange decode(Object result) { + result as List; + return TtsRateValidRange( + minimum: result[0]! as double, + normal: result[1]! as double, + maximum: result[2]! as double, + platform: result[3]! as TtsPlatform, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! TtsRateValidRange || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is FlutterTtsErrorCode) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is IosTextToSpeechAudioCategory) { + buffer.putUint8(130); + writeValue(buffer, value.index); + } else if (value is IosTextToSpeechAudioMode) { + buffer.putUint8(131); + writeValue(buffer, value.index); + } else if (value is IosTextToSpeechAudioCategoryOptions) { + buffer.putUint8(132); + writeValue(buffer, value.index); + } else if (value is TtsPlatform) { + buffer.putUint8(133); + writeValue(buffer, value.index); + } else if (value is Voice) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is TtsResult) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else if (value is TtsProgress) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is TtsRateValidRange) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : FlutterTtsErrorCode.values[value]; + case 130: + final int? value = readValue(buffer) as int?; + return value == null ? null : IosTextToSpeechAudioCategory.values[value]; + case 131: + final int? value = readValue(buffer) as int?; + return value == null ? null : IosTextToSpeechAudioMode.values[value]; + case 132: + final int? value = readValue(buffer) as int?; + return value == null ? null : IosTextToSpeechAudioCategoryOptions.values[value]; + case 133: + final int? value = readValue(buffer) as int?; + return value == null ? null : TtsPlatform.values[value]; + case 134: + return Voice.decode(readValue(buffer)!); + case 135: + return TtsResult.decode(readValue(buffer)!); + case 136: + return TtsProgress.decode(readValue(buffer)!); + case 137: + return TtsRateValidRange.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class TtsHostApi { + /// Constructor for [TtsHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + TtsHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future speak(String text, bool forceFocus) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.TtsHostApi.speak$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([text, forceFocus]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future pause() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.TtsHostApi.pause$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future stop() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.TtsHostApi.stop$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future setSpeechRate(double rate) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.TtsHostApi.setSpeechRate$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([rate]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future setVolume(double volume) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.TtsHostApi.setVolume$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future setPitch(double pitch) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.TtsHostApi.setPitch$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([pitch]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future setVoice(Voice voice) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.TtsHostApi.setVoice$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([voice]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future clearVoice() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.TtsHostApi.clearVoice$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future awaitSpeakCompletion(bool awaitCompletion) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.TtsHostApi.awaitSpeakCompletion$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([awaitCompletion]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future> getLanguages() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.TtsHostApi.getLanguages$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + Future> getVoices() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.TtsHostApi.getVoices$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } +} + +class IosTtsHostApi { + /// Constructor for [IosTtsHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + IosTtsHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future awaitSynthCompletion(bool awaitCompletion) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.IosTtsHostApi.awaitSynthCompletion$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([awaitCompletion]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future synthesizeToFile(String text, String fileName, [bool isFullPath = false,]) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.IosTtsHostApi.synthesizeToFile$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([text, fileName, isFullPath]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future setSharedInstance(bool sharedSession) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setSharedInstance$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([sharedSession]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future autoStopSharedSession(bool autoStop) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.IosTtsHostApi.autoStopSharedSession$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([autoStop]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future setIosAudioCategory(IosTextToSpeechAudioCategory category, List options, {IosTextToSpeechAudioMode mode = IosTextToSpeechAudioMode.defaultMode, }) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setIosAudioCategory$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([category, options, mode]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future getSpeechRateValidRange() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.IosTtsHostApi.getSpeechRateValidRange$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsRateValidRange?)!; + } + } + + Future isLanguageAvailable(String language) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.IosTtsHostApi.isLanguageAvailable$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([language]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + + Future setLanguange(String language) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setLanguange$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([language]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } +} + +class AndroidTtsHostApi { + /// Constructor for [AndroidTtsHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + AndroidTtsHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future awaitSynthCompletion(bool awaitCompletion) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.awaitSynthCompletion$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([awaitCompletion]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future getMaxSpeechInputLength() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getMaxSpeechInputLength$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return (pigeonVar_replyList[0] as int?); + } + } + + Future setEngine(String engine) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setEngine$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([engine]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future> getEngines() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getEngines$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + Future getDefaultEngine() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getDefaultEngine$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return (pigeonVar_replyList[0] as String?); + } + } + + Future getDefaultVoice() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getDefaultVoice$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return (pigeonVar_replyList[0] as Voice?); + } + } + + /// [Future] which invokes the platform specific method for synthesizeToFile + Future synthesizeToFile(String text, String fileName, [bool isFullPath = false,]) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.synthesizeToFile$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([text, fileName, isFullPath]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future isLanguageInstalled(String language) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.isLanguageInstalled$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([language]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + + Future isLanguageAvailable(String language) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.isLanguageAvailable$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([language]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + + Future> areLanguagesInstalled(List languages) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.areLanguagesInstalled$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([languages]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as Map?)!.cast(); + } + } + + Future getSpeechRateValidRange() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getSpeechRateValidRange$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsRateValidRange?)!; + } + } + + Future setSilence(int timems) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setSilence$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([timems]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future setQueueMode(int queueMode) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setQueueMode$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([queueMode]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future setAudioAttributesForNavigation() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setAudioAttributesForNavigation$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } +} + +class MacosTtsHostApi { + /// Constructor for [MacosTtsHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + MacosTtsHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future awaitSynthCompletion(bool awaitCompletion) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.awaitSynthCompletion$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([awaitCompletion]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future getSpeechRateValidRange() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.getSpeechRateValidRange$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsRateValidRange?)!; + } + } + + Future setLanguange(String language) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.setLanguange$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([language]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } + + Future isLanguageAvailable(String language) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.isLanguageAvailable$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([language]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } +} + +abstract class TtsFlutterApi { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + void onSpeakStartCb(); + + void onSpeakCompleteCb(); + + void onSpeakPauseCb(); + + void onSpeakResumeCb(); + + void onSpeakCancelCb(); + + void onSpeakProgressCb(TtsProgress progress); + + void onSpeakErrorCb(String error); + + void onSynthStartCb(); + + void onSynthCompleteCb(); + + void onSynthErrorCb(String error); + + static void setUp(TtsFlutterApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakStartCb$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + api.onSpeakStartCb(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakCompleteCb$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + api.onSpeakCompleteCb(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakPauseCb$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + api.onSpeakPauseCb(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakResumeCb$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + api.onSpeakResumeCb(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakCancelCb$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + api.onSpeakCancelCb(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakProgressCb$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakProgressCb was null.'); + final List args = (message as List?)!; + final TtsProgress? arg_progress = (args[0] as TtsProgress?); + assert(arg_progress != null, + 'Argument for dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakProgressCb was null, expected non-null TtsProgress.'); + try { + api.onSpeakProgressCb(arg_progress!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakErrorCb$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakErrorCb was null.'); + final List args = (message as List?)!; + final String? arg_error = (args[0] as String?); + assert(arg_error != null, + 'Argument for dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakErrorCb was null, expected non-null String.'); + try { + api.onSpeakErrorCb(arg_error!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthStartCb$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + api.onSynthStartCb(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthCompleteCb$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + api.onSynthCompleteCb(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthErrorCb$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthErrorCb was null.'); + final List args = (message as List?)!; + final String? arg_error = (args[0] as String?); + assert(arg_error != null, + 'Argument for dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthErrorCb was null, expected non-null String.'); + try { + api.onSynthErrorCb(arg_error!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/macos/Classes/FlutterTtsPlugin.swift b/macos/Classes/FlutterTtsPlugin.swift index f30bca60..715f07c7 100644 --- a/macos/Classes/FlutterTtsPlugin.swift +++ b/macos/Classes/FlutterTtsPlugin.swift @@ -1,386 +1,437 @@ +import AVFoundation import FlutterMacOS import Foundation -import AVFoundation -public class FlutterTtsPlugin: NSObject, FlutterPlugin, AVSpeechSynthesizerDelegate { - final var iosAudioCategoryKey = "iosAudioCategoryKey" - final var iosAudioCategoryOptionsKey = "iosAudioCategoryOptionsKey" - - let synthesizer = AVSpeechSynthesizer() - var language: String = AVSpeechSynthesisVoice.currentLanguageCode() - var rate: Float = AVSpeechUtteranceDefaultSpeechRate - var languages = Set() - var volume: Float = 1.0 - var pitch: Float = 1.0 - var voice: AVSpeechSynthesisVoice? - var awaitSpeakCompletion: Bool = false - var awaitSynthCompletion: Bool = false - var speakResult: FlutterResult! - var synthResult: FlutterResult! - - var channel = FlutterMethodChannel() - init(channel: FlutterMethodChannel) { - super.init() - self.channel = channel - synthesizer.delegate = self - setLanguages() - } - - private func setLanguages() { - for voice in AVSpeechSynthesisVoice.speechVoices(){ - self.languages.insert(voice.language) - } - } - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "flutter_tts", binaryMessenger: registrar.messenger) - let instance = FlutterTtsPlugin(channel: channel) - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "speak": - let text: String = call.arguments as! String - self.speak(text: text, result: result) - break - case "awaitSpeakCompletion": - self.awaitSpeakCompletion = call.arguments as! Bool - result(1) - break - case "awaitSynthCompletion": - self.awaitSynthCompletion = call.arguments as! Bool - result(1) - break - case "synthesizeToFile": - guard let args = call.arguments as? [String: Any] else { - result("iOS could not recognize flutter arguments in method: (sendParams)") - return - } - let text = args["text"] as! String - let fileName = args["fileName"] as! String - self.synthesizeToFile(text: text, fileName: fileName, result: result) - break - case "pause": - self.pause(result: result) - break - case "setLanguage": - let language: String = call.arguments as! String - self.setLanguage(language: language, result: result) - break - case "setSpeechRate": - let rate: Double = call.arguments as! Double - self.setRate(rate: Float(rate)) - result(1) - break - case "setVolume": - let volume: Double = call.arguments as! Double - self.setVolume(volume: Float(volume), result: result) - break - case "setPitch": - let pitch: Double = call.arguments as! Double - self.setPitch(pitch: Float(pitch), result: result) - break - case "stop": - self.stop() - result(1) - break - case "getLanguages": - self.getLanguages(result: result) - break - case "getSpeechRateValidRange": - self.getSpeechRateValidRange(result: result) - break - case "isLanguageAvailable": - let language: String = call.arguments as! String - self.isLanguageAvailable(language: language, result: result) - break - case "getVoices": - self.getVoices(result: result) - break - case "setVoice": - guard let args = call.arguments as? [String: String] else { - result("iOS could not recognize flutter arguments in method: (sendParams)") - return - } - self.setVoice(voice: args, result: result) - break - case "autoStopSharedSession": - // MacOS does not have a shared audio session so just accept the call - result(1) - break - default: - result(FlutterMethodNotImplemented) - } - } - - private func speak(text: String, result: @escaping FlutterResult) { - if (self.synthesizer.isPaused) { - if (self.synthesizer.continueSpeaking()) { - if self.awaitSpeakCompletion { - self.speakResult = result +extension FlutterTtsErrorCode { + func toStrCode() -> String { + return "FlutterTtsErrorCode.\(rawValue)" + } +} + +let kVoiceSelectionNotSuported = "voice selection is not supported below Macos 10.15" + +/// 带泛型结果类型 R 的 completion 别名 +typealias ResultCallback = (Result) -> Void + +public class FlutterTtsPlugin: NSObject, FlutterPlugin, AVSpeechSynthesizerDelegate, TtsHostApi, + MacosTtsHostApi +{ + final var iosAudioCategoryKey = "iosAudioCategoryKey" + final var iosAudioCategoryOptionsKey = "iosAudioCategoryOptionsKey" + + let synthesizer = AVSpeechSynthesizer() + var language: String = AVSpeechSynthesisVoice.currentLanguageCode() + var rate: Float = AVSpeechUtteranceDefaultSpeechRate + var languages = Set() + var volume: Float = 1.0 + var pitch: Float = 1.0 + var voice: AVSpeechSynthesisVoice? + var awaitSpeakCompletion: Bool = false + var awaitSynthCompletion: Bool = false + var speakResult: ResultCallback! + var synthResult: ResultCallback! + var flutterApi: TtsFlutterApi + init(flutterApi: TtsFlutterApi) { + self.flutterApi = flutterApi + super.init() + synthesizer.delegate = self + setLanguages() + } + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = FlutterTtsPlugin( + flutterApi: TtsFlutterApi(binaryMessenger: registrar.messenger)) + TtsHostApiSetup.setUp(binaryMessenger: registrar.messenger, api: instance) + MacosTtsHostApiSetup.setUp(binaryMessenger: registrar.messenger, api: instance) + } + + func setLanguange( + language: String, completion: @escaping (Result) -> Void + ) { + setLanguageImpl(language: language, completion: completion) + } + + func speak( + text: String, forceFocus: Bool, completion: @escaping (Result) -> Void + ) { + speakImpl(text: text, completion: completion) + } + + func pause(completion: @escaping (Result) -> Void) { + pauseImpl(completion: completion) + } + + func stop(completion: @escaping (Result) -> Void) { + stopImpl() + completion(Result.success(TtsResult(success: true))) + } + + func setSpeechRate(rate: Double, completion: @escaping (Result) -> Void) { + setRateImpl(rate: Float(rate)) + completion(Result.success(TtsResult(success: true))) + } + + func setVolume(volume: Double, completion: @escaping (Result) -> Void) { + setVolumeImpl(volume: Float(volume), completion: completion) + } + + func setPitch(pitch: Double, completion: @escaping (Result) -> Void) { + setPitchImpl(pitch: Float(pitch), completion: completion) + } + + func setVoice(voice: Voice, completion: @escaping (Result) -> Void) { + setVoiceImpl(voice: voice, completion: completion) + } + + func clearVoice(completion: @escaping (Result) -> Void) { + completion(Result.success(TtsResult(success: true))) + } + + func awaitSpeakCompletion( + awaitCompletion: Bool, completion: @escaping (Result) -> Void + ) { + awaitSpeakCompletion = awaitCompletion + completion(Result.success(TtsResult(success: true))) + } + + func getLanguages(completion: @escaping (Result<[String], any Error>) -> Void) { + getLanguagesImpl(completion: completion) + } + + func getVoices(completion: @escaping (Result<[Voice], any Error>) -> Void) { + getVoicesImpl(completion: completion) + } + + func awaitSynthCompletion( + awaitCompletion: Bool, completion: @escaping (Result) -> Void + ) { + awaitSynthCompletion = awaitCompletion + completion(Result.success(TtsResult(success: true))) + } + + func synthesizeToFile( + text: String, + fileName: String, + isFullPath: Bool, + completion: @escaping (Result) -> Void + ) { + synthesizeToFileImpl(text: text, fileName: fileName, completion: completion) + } + + func getSpeechRateValidRange( + completion: @escaping (Result) -> Void + ) { + getSpeechRateValidRangeImpl(completion: completion) + } + + func isLanguageAvailable( + language: String, completion: @escaping (Result) -> Void + ) { + isLanguageAvailableImpl(language: language, completion: completion) + } + + private func setLanguages() { + for voice in AVSpeechSynthesisVoice.speechVoices() { + languages.insert(voice.language) + } + } + + private func speakImpl(text: String, completion: @escaping ResultCallback) { + if synthesizer.isPaused { + if synthesizer.continueSpeaking() { + if awaitSpeakCompletion { + speakResult = completion + } else { + completion(Result.success(TtsResult(success: true))) + } + } else { + completion(Result.success(TtsResult(success: false))) + } + } else { + let utterance = AVSpeechUtterance(string: text) + if voice != nil { + utterance.voice = voice! + } else { + utterance.voice = AVSpeechSynthesisVoice(language: language) + } + utterance.rate = rate + utterance.volume = volume + utterance.pitchMultiplier = pitch + + synthesizer.speak(utterance) + if awaitSpeakCompletion { + speakResult = completion + } else { + completion(Result.success(TtsResult(success: true))) + } + } + } + + private func synthesizeToFileImpl( + text: String, fileName: String, completion: @escaping ResultCallback + ) { + var output: AVAudioFile? + var failed = false + let utterance = AVSpeechUtterance(string: text) + + if #available(macOS 10.15, *) { + self.synthesizer.write(utterance) { (buffer: AVAudioBuffer) in + guard let pcmBuffer = buffer as? AVAudioPCMBuffer else { + NSLog("unknow buffer type: \(buffer)") + failed = true + return + } + if pcmBuffer.frameLength == 0 { + // finished + } else { + // append buffer to file + let fileURL = FileManager.default.urls( + for: .documentDirectory, in: .userDomainMask + ) + .first!.appendingPathComponent(fileName) + NSLog("Saving utterance to file: \(fileURL.absoluteString)") + + if output == nil { + do { + output = try AVAudioFile( + forWriting: fileURL, + settings: pcmBuffer.format.settings, + commonFormat: .pcmFormatFloat32, + interleaved: false + ) + } catch { + NSLog(error.localizedDescription) + failed = true + return + } + } + + try! output!.write(from: pcmBuffer) + } + } + } else { + completion(Result.failure(PigeonError(code: FlutterTtsErrorCode.notSupportedOSVersion.toStrCode(), + message: kVoiceSelectionNotSuported, + details: nil))) + } + + if failed { + completion(Result.success(TtsResult(success: false))) + } + + if awaitSynthCompletion { + synthResult = completion } else { - result(1) + completion(Result.success(TtsResult(success: true))) } - } else { - result(0) - } - } else { - let utterance = AVSpeechUtterance(string: text) - if self.voice != nil { - utterance.voice = self.voice! - } else { - utterance.voice = AVSpeechSynthesisVoice(language: self.language) - } - utterance.rate = self.rate - utterance.volume = self.volume - utterance.pitchMultiplier = self.pitch - - self.synthesizer.speak(utterance) - if self.awaitSpeakCompletion { - self.speakResult = result - } else { - result(1) - } - } - } - - private func synthesizeToFile(text: String, fileName: String, result: @escaping FlutterResult) { - var output: AVAudioFile? - var failed = false - let utterance = AVSpeechUtterance(string: text) - - if #available(iOS 13.0, *) { - self.synthesizer.write(utterance) { (buffer: AVAudioBuffer) in - guard let pcmBuffer = buffer as? AVAudioPCMBuffer else { - NSLog("unknow buffer type: \(buffer)") - failed = true - return + } + + private func pauseImpl(completion: ResultCallback) { + if synthesizer.pauseSpeaking(at: AVSpeechBoundary.word) { + completion(Result.success(TtsResult(success: true))) + } else { + completion(Result.success(TtsResult(success: false))) + } + } + + private func setLanguageImpl(language: String, completion: ResultCallback) { + if !(languages.contains(where: { + $0.range(of: language, options: [.caseInsensitive, .anchored]) != nil + })) { + completion(Result.success(TtsResult(success: false))) + } else { + self.language = language + voice = nil + completion(Result.success(TtsResult(success: true))) + } + } + + private func setRateImpl(rate: Float) { + self.rate = rate + } + + private func setVolumeImpl(volume: Float, completion: ResultCallback) { + if volume >= 0.0 && volume <= 1.0 { + self.volume = volume + completion(Result.success(TtsResult(success: true))) + } else { + completion(Result.success(TtsResult(success: false))) } - if pcmBuffer.frameLength == 0 { - // finished + } + + private func setPitchImpl(pitch: Float, completion: ResultCallback) { + if volume >= 0.5 && volume <= 2.0 { + self.pitch = pitch + completion(Result.success(TtsResult(success: true))) } else { - // append buffer to file - let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(fileName) - NSLog("Saving utterance to file: \(fileURL.absoluteString)") - - if output == nil { - do { - output = try AVAudioFile( - forWriting: fileURL, - settings: pcmBuffer.format.settings, - commonFormat: .pcmFormatFloat32, - interleaved: false) - } catch { - NSLog(error.localizedDescription) - failed = true - return + completion(Result.success(TtsResult(success: false))) + } + } + + private func stopImpl() { + synthesizer.stopSpeaking(at: AVSpeechBoundary.immediate) + } + + private func getLanguagesImpl(completion: ResultCallback<[String]>) { + completion(Result.success(Array(languages))) + } + + private func getSpeechRateValidRangeImpl(completion: ResultCallback) { + let validSpeechRateRange = TtsRateValidRange( + minimum: Double(AVSpeechUtteranceMinimumSpeechRate), + normal: Double(AVSpeechUtteranceDefaultSpeechRate), + maximum: Double(AVSpeechUtteranceMaximumSpeechRate), + platform: TtsPlatform.ios + ) + completion(Result.success(validSpeechRateRange)) + } + + private func isLanguageAvailableImpl(language: String, completion: ResultCallback) { + var isAvailable = false + if languages.contains(where: { + $0.range(of: language, options: [.caseInsensitive, .anchored]) != nil + }) { + isAvailable = true + } + completion(Result.success(isAvailable)) + } + + private func getVoicesImpl(completion: ResultCallback<[Voice]>) { + if #available(macOS 10.15, *) { + var voices = [Voice]() + for voice in AVSpeechSynthesisVoice.speechVoices() { + var gender: String? = nil + if #available(macOS 10.15, *) { + gender = voice.gender.stringValue + } + let voiceDict = Voice(name: voice.name, + locale: voice.language, + gender: gender, + quality: voice.quality.stringValue, + identifier: voice.identifier) + + voices.append(voiceDict) } - } - - try! output!.write(from: pcmBuffer) + completion(Result.success(voices)) + } else { + completion(Result.failure(PigeonError(code: FlutterTtsErrorCode.notSupportedOSVersion.toStrCode(), + message: kVoiceSelectionNotSuported, + details: nil))) } - } - } else { - result("Unsupported iOS version") - } - if failed { - result(0) - } - if self.awaitSynthCompletion { - self.synthResult = result - } else { - result(1) - } - } - - private func pause(result: FlutterResult) { - if (self.synthesizer.pauseSpeaking(at: AVSpeechBoundary.word)) { - result(1) - } else { - result(0) - } - } - - private func setLanguage(language: String, result: FlutterResult) { - if !(self.languages.contains(where: {$0.range(of: language, options: [.caseInsensitive, .anchored]) != nil})) { - result(0) - } else { - self.language = language - self.voice = nil - result(1) - } - } - - private func setRate(rate: Float) { - self.rate = rate - } - - private func setVolume(volume: Float, result: FlutterResult) { - if (volume >= 0.0 && volume <= 1.0) { - self.volume = volume - result(1) - } else { - result(0) - } - } - - private func setPitch(pitch: Float, result: FlutterResult) { - if (volume >= 0.5 && volume <= 2.0) { - self.pitch = pitch - result(1) - } else { - result(0) - } - } - - private func stop() { - self.synthesizer.stopSpeaking(at: AVSpeechBoundary.immediate) - } - - private func getLanguages(result: FlutterResult) { - result(Array(self.languages)) - } - - private func getSpeechRateValidRange(result: FlutterResult) { - let validSpeechRateRange: [String:String] = [ - "min": String(AVSpeechUtteranceMinimumSpeechRate), - "normal": String(AVSpeechUtteranceDefaultSpeechRate), - "max": String(AVSpeechUtteranceMaximumSpeechRate), - "platform": "ios" - ] - result(validSpeechRateRange) - } - - private func isLanguageAvailable(language: String, result: FlutterResult) { - var isAvailable: Bool = false - if (self.languages.contains(where: {$0.range(of: language, options: [.caseInsensitive, .anchored]) != nil})) { - isAvailable = true - } - result(isAvailable); - } - - private func getVoices(result: FlutterResult) { - if #available(macOS 10.15, *) { - let voices = NSMutableArray() - var voiceDict: [String: String] = [:] - for voice in AVSpeechSynthesisVoice.speechVoices() { - voiceDict["name"] = voice.name - voiceDict["locale"] = voice.language - voiceDict["quality"] = voice.quality.stringValue + } + + private func setVoiceImpl(voice: Voice, completion: ResultCallback) { if #available(macOS 10.15, *) { - voiceDict["gender"] = voice.gender.stringValue + // Check if identifier exists and is not empty + if let identifier = voice.identifier, !identifier.isEmpty { + // Find the voice by identifier + if let selectedVoice = AVSpeechSynthesisVoice(identifier: identifier) { + self.voice = selectedVoice + self.language = selectedVoice.language + completion(Result.success(TtsResult(success: true))) + return + } + } + + // If no valid identifier, search by name and locale, then prioritize by quality + let name = voice.name + let locale = voice.locale + let matchingVoices = AVSpeechSynthesisVoice.speechVoices().filter { + $0.name == name && $0.language == locale + } + + if !matchingVoices.isEmpty { + // Sort voices by quality: premium (if available) > enhanced > others + let sortedVoices = matchingVoices.sorted { voice1, voice2 -> Bool in + let quality1 = voice1.quality + let quality2 = voice2.quality + + // macOS 13.0+ supports premium quality + if #available(macOS 13.0, *) { + if quality1 == .premium { + return true + } else if quality1 == .enhanced && quality2 != .premium { + return true + } else { + return false + } + } else { + // Fallback for macOS versions before 13.0 (no premium) + if quality1 == .enhanced { + return true + } else { + return false + } + } + } + + // Select the highest quality voice + if let selectedVoice = sortedVoices.first { + self.voice = selectedVoice + self.language = selectedVoice.language + completion(Result.success(TtsResult(success: true))) + return + } + } + + // No matching voice found + completion(Result.success(TtsResult(success: false))) + } else { + completion(Result.failure(PigeonError(code: FlutterTtsErrorCode.notSupportedOSVersion.toStrCode(), + message: kVoiceSelectionNotSuported, + details: nil))) + } + } + + public func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance + ) { + if awaitSpeakCompletion { + speakResult(Result.success(TtsResult(success: true))) } - voiceDict["identifier"] = voice.identifier - voices.add(voiceDict) - } - result(voices) - } else { - // Since voice selection is not supported below iOS 9, make voice getter and setter - // have the same bahavior as language selection. - getLanguages(result: result) - } - } - - - - private func setVoice(voice: [String: String], result: FlutterResult) { - if #available(iOS 9.0, *) { - // Check if identifier exists and is not empty - if let identifier = voice["identifier"], !identifier.isEmpty { - // Find the voice by identifier - if let selectedVoice = AVSpeechSynthesisVoice(identifier: identifier) { - self.voice = selectedVoice - self.language = selectedVoice.language - result(1) - return - } - } - - // If no valid identifier, search by name and locale, then prioritize by quality - if let name = voice["name"], let locale = voice["locale"] { - let matchingVoices = AVSpeechSynthesisVoice.speechVoices().filter { $0.name == name && $0.language == locale } - - if !matchingVoices.isEmpty { - // Sort voices by quality: premium (if available) > enhanced > others - let sortedVoices = matchingVoices.sorted { (voice1, voice2) -> Bool in - let quality1 = voice1.quality - let quality2 = voice2.quality - - // macOS 13.0+ supports premium quality - if #available(macOS 13.0, *) { - if quality1 == .premium { - return true - } else if quality1 == .enhanced && quality2 != .premium { - return true - } else { - return false - } - } else { - // Fallback for macOS versions before 13.0 (no premium) - if quality1 == .enhanced { - return true - } else { - return false - } - } - } - - // Select the highest quality voice - if let selectedVoice = sortedVoices.first { - self.voice = selectedVoice - self.language = selectedVoice.language - result(1) - return - } - } - } - - // No matching voice found - result(0) - } else { - // Handle older iOS versions if needed - setLanguage(language: voice["name"]!, result: result) - } - } - - public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { - if self.awaitSpeakCompletion { - self.speakResult(1) - } - if self.awaitSynthCompletion { - self.synthResult(1) - } - self.channel.invokeMethod("speak.onComplete", arguments: nil) - } - - public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) { - self.channel.invokeMethod("speak.onStart", arguments: nil) - } - - public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) { - self.channel.invokeMethod("speak.onPause", arguments: nil) - } - - public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) { - self.channel.invokeMethod("speak.onContinue", arguments: nil) - } - - public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) { - self.channel.invokeMethod("speak.onCancel", arguments: nil) - } - - public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) { - let nsWord = utterance.speechString as NSString - let data: [String:String] = [ - "text": utterance.speechString, - "start": String(characterRange.location), - "end": String(characterRange.location + characterRange.length), - "word": nsWord.substring(with: characterRange) - ] - self.channel.invokeMethod("speak.onProgress", arguments: data) - } + if awaitSynthCompletion { + synthResult(Result.success(TtsResult(success: true))) + } + flutterApi.onSpeakCompleteCb { _ in } + } + + public func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance + ) { + flutterApi.onSpeakStartCb { _ in } + } + public func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance + ) { + flutterApi.onSpeakPauseCb { _ in } + } + + public func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance + ) { + flutterApi.onSpeakResumeCb { _ in } + } + + public func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance + ) { + flutterApi.onSpeakCancelCb { _ in } + } + + public func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, + utterance: AVSpeechUtterance + ) { + let nsWord = utterance.speechString as NSString + let data = TtsProgress( + text: utterance.speechString, + start: Int64(characterRange.location), + end: Int64(characterRange.location + characterRange.length), + word: nsWord.substring(with: characterRange) + ) + flutterApi.onSpeakProgressCb(progress: data) { _ in } + } } extension AVSpeechSynthesisVoiceQuality { @@ -392,6 +443,8 @@ extension AVSpeechSynthesisVoiceQuality { return "premium" case .enhanced: return "enhanced" + default: + return "unknown" } } } @@ -406,6 +459,8 @@ extension AVSpeechSynthesisVoiceGender { return "female" case .unspecified: return "unspecified" + default: + return "unknown" } } } diff --git a/macos/Classes/message.g.swift b/macos/Classes/message.g.swift new file mode 100644 index 00000000..d2539406 --- /dev/null +++ b/macos/Classes/message.g.swift @@ -0,0 +1,1548 @@ +// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Sendable? + + init(code: String, message: String?, details: Sendable?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func createConnectionError(withChannelName channelName: String) -> PigeonError { + return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "") +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +func deepEqualsmessage(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case is (Void, Void): + return true + + case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): + return cleanLhsHashable == cleanRhsHashable + + case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): + guard cleanLhsArray.count == cleanRhsArray.count else { return false } + for (index, element) in cleanLhsArray.enumerated() { + if !deepEqualsmessage(element, cleanRhsArray[index]) { + return false + } + } + return true + + case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } + for (key, cleanLhsValue) in cleanLhsDictionary { + guard cleanRhsDictionary.index(forKey: key) != nil else { return false } + if !deepEqualsmessage(cleanLhsValue, cleanRhsDictionary[key]!) { + return false + } + } + return true + + default: + // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. + return false + } +} + +func deepHashmessage(value: Any?, hasher: inout Hasher) { + if let valueList = value as? [AnyHashable] { + for item in valueList { deepHashmessage(value: item, hasher: &hasher) } + return + } + + if let valueDict = value as? [AnyHashable: AnyHashable] { + for key in valueDict.keys { + hasher.combine(key) + deepHashmessage(value: valueDict[key]!, hasher: &hasher) + } + return + } + + if let hashableValue = value as? AnyHashable { + hasher.combine(hashableValue.hashValue) + } + + return hasher.combine(String(describing: value)) +} + + + +enum FlutterTtsErrorCode: Int { + /// general error code for TTS engine not available. + case ttsNotAvailable = 0 + /// The TTS engine failed to initialize in n second. + /// 1 second is the default timeout. + /// e.g. Some Android custom ROMS may trim TTS service, + /// and third party TTS engine may fail to initialize due to battery optimization. + case ttsInitTimeout = 1 + /// not supported on current os version + case notSupportedOSVersion = 2 +} + +/// Audio session category identifiers for iOS. +/// +/// See also: +/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/category +enum IosTextToSpeechAudioCategory: Int { + /// The default audio session category. + /// + /// Your audio is silenced by screen locking and by the Silent switch. + /// + /// By default, using this category implies that your app’s audio + /// is nonmixable—activating your session will interrupt + /// any other audio sessions which are also nonmixable. + /// To allow mixing, use the [ambient] category instead. + case ambientSolo = 0 + /// The category for an app in which sound playback is nonprimary — that is, + /// your app also works with the sound turned off. + /// + /// This category is also appropriate for “play-along” apps, + /// such as a virtual piano that a user plays while the Music app is playing. + /// When you use this category, audio from other apps mixes with your audio. + /// Screen locking and the Silent switch (on iPhone, the Ring/Silent switch) silence your audio. + case ambient = 1 + /// The category for playing recorded music or other sounds + /// that are central to the successful use of your app. + /// + /// When using this category, your app audio continues + /// with the Silent switch set to silent or when the screen locks. + /// + /// By default, using this category implies that your app’s audio + /// is nonmixable—activating your session will interrupt + /// any other audio sessions which are also nonmixable. + /// To allow mixing for this category, use the + /// [IosTextToSpeechAudioCategoryOptions.mixWithOthers] option. + case playback = 2 + /// The category for recording (input) and playback (output) of audio, + /// such as for a Voice over Internet Protocol (VoIP) app. + /// + /// Your audio continues with the Silent switch set to silent and with the screen locked. + /// This category is appropriate for simultaneous recording and playback, + /// and also for apps that record and play back, but not simultaneously. + case playAndRecord = 3 +} + +/// Audio session mode identifiers for iOS. +/// +/// See also: +/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/mode +enum IosTextToSpeechAudioMode: Int { + /// The default audio session mode. + /// + /// You can use this mode with every [IosTextToSpeechAudioCategory]. + case defaultMode = 0 + /// A mode that the GameKit framework sets on behalf of an application + /// that uses GameKit’s voice chat service. + /// + /// This mode is valid only with the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// + /// Don’t set this mode directly. If you need similar behavior and aren’t + /// using a `GKVoiceChat` object, use [voiceChat] or [videoChat] instead. + case gameChat = 1 + /// A mode that indicates that your app is performing measurement of audio input or output. + /// + /// Use this mode for apps that need to minimize the amount of + /// system-supplied signal processing to input and output signals. + /// If recording on devices with more than one built-in microphone, + /// the session uses the primary microphone. + /// + /// For use with the [IosTextToSpeechAudioCategory.playback] or + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// + /// **Important:** This mode disables some dynamics processing on input and output signals, + /// resulting in a lower-output playback level. + case measurement = 2 + /// A mode that indicates that your app is playing back movie content. + /// + /// When you set this mode, the audio session uses signal processing to enhance + /// movie playback for certain audio routes such as built-in speaker or headphones. + /// You may only use this mode with the + /// [IosTextToSpeechAudioCategory.playback] category. + case moviePlayback = 3 + /// A mode used for continuous spoken audio to pause the audio when another app plays a short audio prompt. + /// + /// This mode is appropriate for apps that play continuous spoken audio, + /// such as podcasts or audio books. Setting this mode indicates that your app + /// should pause, rather than duck, its audio if another app plays + /// a spoken audio prompt. After the interrupting app’s audio ends, you can + /// resume your app’s audio playback. + case spokenAudio = 4 + /// A mode that indicates that your app is engaging in online video conferencing. + /// + /// Use this mode for video chat apps that use the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// When you set this mode, the audio session optimizes the device’s tonal + /// equalization for voice. It also reduces the set of allowable audio routes + /// to only those appropriate for video chat. + /// + /// Using this mode has the side effect of enabling the + /// [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. + case videoChat = 5 + /// A mode that indicates that your app is recording a movie. + /// + /// This mode is valid only with the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// On devices with more than one built-in microphone, + /// the audio session uses the microphone closest to the video camera. + /// + /// Use this mode to ensure that the system provides appropriate audio-signal processing. + case videoRecording = 6 + /// A mode that indicates that your app is performing two-way voice communication, + /// such as using Voice over Internet Protocol (VoIP). + /// + /// Use this mode for Voice over IP (VoIP) apps that use the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// When you set this mode, the session optimizes the device’s tonal + /// equalization for voice and reduces the set of allowable audio routes + /// to only those appropriate for voice chat. + /// + /// Using this mode has the side effect of enabling the + /// [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. + case voiceChat = 7 + /// A mode that indicates that your app plays audio using text-to-speech. + /// + /// Setting this mode allows for different routing behaviors when your app + /// is connected to certain audio devices, such as CarPlay. + /// An example of an app that uses this mode is a turn-by-turn navigation app + /// that plays short prompts to the user. + /// + /// Typically, apps of the same type also configure their sessions to use the + /// [IosTextToSpeechAudioCategoryOptions.duckOthers] and + /// [IosTextToSpeechAudioCategoryOptions.interruptSpokenAudioAndMixWithOthers] options. + case voicePrompt = 8 +} + +/// Audio session category options for iOS. +/// +/// See also: +/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions +enum IosTextToSpeechAudioCategoryOptions: Int { + /// An option that indicates whether audio from this session mixes with audio + /// from active sessions in other audio apps. + /// + /// You can set this option explicitly only if the audio session category + /// is [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// If you set the audio session category to [IosTextToSpeechAudioCategory.ambient], + /// the session automatically sets this option. + /// Likewise, setting the [duckOthers] or [interruptSpokenAudioAndMixWithOthers] + /// options also enables this option. + /// + /// If you set this option, your app mixes its audio with audio playing + /// in background apps, such as the Music app. + case mixWithOthers = 0 + /// An option that reduces the volume of other audio sessions while audio + /// from this session plays. + /// + /// You can set this option only if the audio session category is + /// [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// Setting it implicitly sets the [mixWithOthers] option. + /// + /// Use this option to mix your app’s audio with that of others. + /// While your app plays its audio, the system reduces the volume of other + /// audio sessions to make yours more prominent. If your app provides + /// occasional spoken audio, such as in a turn-by-turn navigation app + /// or an exercise app, you should also set the [interruptSpokenAudioAndMixWithOthers] option. + /// + /// Note that ducking begins when you activate your app’s audio session + /// and ends when you deactivate the session. + /// + /// See also: + /// * [FlutterTts.setSharedInstance] + case duckOthers = 1 + /// An option that determines whether to pause spoken audio content + /// from other sessions when your app plays its audio. + /// + /// You can set this option only if the audio session category is + /// [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// Setting this option also sets [mixWithOthers]. + /// + /// If you set this option, the system mixes your audio with other + /// audio sessions, but interrupts (and stops) audio sessions that use the + /// [IosTextToSpeechAudioMode.spokenAudio] audio session mode. + /// It pauses the audio from other apps as long as your session is active. + /// After your audio session deactivates, the system resumes the interrupted app’s audio. + /// + /// Set this option if your app’s audio is occasional and spoken, + /// such as in a turn-by-turn navigation app or an exercise app. + /// This avoids intelligibility problems when two spoken audio apps mix. + /// If you set this option, also set the [duckOthers] option unless + /// you have a specific reason not to. Ducking other audio, rather than + /// interrupting it, is appropriate when the other audio isn’t spoken audio. + case interruptSpokenAudioAndMixWithOthers = 2 + /// An option that determines whether Bluetooth hands-free devices appear + /// as available input routes. + /// + /// You can set this option only if the audio session category is + /// [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// + /// You’re required to set this option to allow routing audio input and output + /// to a paired Bluetooth Hands-Free Profile (HFP) device. + /// If you clear this option, paired Bluetooth HFP devices don’t show up + /// as available audio input routes. + case allowBluetooth = 3 + /// An option that determines whether you can stream audio from this session + /// to Bluetooth devices that support the Advanced Audio Distribution Profile (A2DP). + /// + /// A2DP is a stereo, output-only profile intended for higher bandwidth + /// audio use cases, such as music playback. + /// The system automatically routes to A2DP ports if you configure an + /// app’s audio session to use the [IosTextToSpeechAudioCategory.ambient], + /// [IosTextToSpeechAudioCategory.ambientSolo], or + /// [IosTextToSpeechAudioCategory.playback] categories. + /// + /// Starting with iOS 10.0, apps using the + /// [IosTextToSpeechAudioCategory.playAndRecord] category may also allow + /// routing output to paired Bluetooth A2DP devices. To enable this behavior, + /// pass this category option when setting your audio session’s category. + /// + /// Note: If this option and the [allowBluetooth] option are both set, + /// when a single device supports both the Hands-Free Profile (HFP) and A2DP, + /// the system gives hands-free ports a higher priority for routing. + case allowBluetoothA2DP = 4 + /// An option that determines whether you can stream audio + /// from this session to AirPlay devices. + /// + /// Setting this option enables the audio session to route audio output + /// to AirPlay devices. You can only explicitly set this option if the + /// audio session’s category is set to [IosTextToSpeechAudioCategory.playAndRecord]. + /// For most other audio session categories, the system sets this option implicitly. + case allowAirPlay = 5 + /// An option that determines whether audio from the session defaults to the built-in speaker instead of the receiver. + /// + /// You can set this option only when using the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// It’s used to modify the category’s routing behavior so that audio + /// is always routed to the speaker rather than the receiver if + /// no other accessories, such as headphones, are in use. + /// + /// When using this option, the system honors user gestures. + /// For example, plugging in a headset causes the route to change to + /// headset mic/headphones, and unplugging the headset causes the route + /// to change to built-in mic/speaker (as opposed to built-in mic/receiver) + /// when you’ve set this override. + /// + /// In the case of using a USB input-only accessory, audio input + /// comes from the accessory, and the system routes audio to the headphones, + /// if attached, or to the speaker if the headphones aren’t plugged in. + /// The use case is to route audio to the speaker instead of the receiver + /// in cases where the audio would normally go to the receiver. + case defaultToSpeaker = 6 +} + +enum TtsPlatform: Int { + case android = 0 + case ios = 1 +} + +/// Generated class from Pigeon that represents data sent in messages. +struct Voice: Hashable { + var name: String + var locale: String + var gender: String? = nil + var quality: String? = nil + var identifier: String? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> Voice? { + let name = pigeonVar_list[0] as! String + let locale = pigeonVar_list[1] as! String + let gender: String? = nilOrValue(pigeonVar_list[2]) + let quality: String? = nilOrValue(pigeonVar_list[3]) + let identifier: String? = nilOrValue(pigeonVar_list[4]) + + return Voice( + name: name, + locale: locale, + gender: gender, + quality: quality, + identifier: identifier + ) + } + func toList() -> [Any?] { + return [ + name, + locale, + gender, + quality, + identifier, + ] + } + static func == (lhs: Voice, rhs: Voice) -> Bool { + return deepEqualsmessage(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashmessage(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct TtsResult: Hashable { + var success: Bool + var message: String? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> TtsResult? { + let success = pigeonVar_list[0] as! Bool + let message: String? = nilOrValue(pigeonVar_list[1]) + + return TtsResult( + success: success, + message: message + ) + } + func toList() -> [Any?] { + return [ + success, + message, + ] + } + static func == (lhs: TtsResult, rhs: TtsResult) -> Bool { + return deepEqualsmessage(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashmessage(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct TtsProgress: Hashable { + var text: String + var start: Int64 + var end: Int64 + var word: String + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> TtsProgress? { + let text = pigeonVar_list[0] as! String + let start = pigeonVar_list[1] as! Int64 + let end = pigeonVar_list[2] as! Int64 + let word = pigeonVar_list[3] as! String + + return TtsProgress( + text: text, + start: start, + end: end, + word: word + ) + } + func toList() -> [Any?] { + return [ + text, + start, + end, + word, + ] + } + static func == (lhs: TtsProgress, rhs: TtsProgress) -> Bool { + return deepEqualsmessage(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashmessage(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct TtsRateValidRange: Hashable { + var minimum: Double + var normal: Double + var maximum: Double + var platform: TtsPlatform + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> TtsRateValidRange? { + let minimum = pigeonVar_list[0] as! Double + let normal = pigeonVar_list[1] as! Double + let maximum = pigeonVar_list[2] as! Double + let platform = pigeonVar_list[3] as! TtsPlatform + + return TtsRateValidRange( + minimum: minimum, + normal: normal, + maximum: maximum, + platform: platform + ) + } + func toList() -> [Any?] { + return [ + minimum, + normal, + maximum, + platform, + ] + } + static func == (lhs: TtsRateValidRange, rhs: TtsRateValidRange) -> Bool { + return deepEqualsmessage(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashmessage(value: toList(), hasher: &hasher) + } +} + +private class MessagePigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return FlutterTtsErrorCode(rawValue: enumResultAsInt) + } + return nil + case 130: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return IosTextToSpeechAudioCategory(rawValue: enumResultAsInt) + } + return nil + case 131: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return IosTextToSpeechAudioMode(rawValue: enumResultAsInt) + } + return nil + case 132: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return IosTextToSpeechAudioCategoryOptions(rawValue: enumResultAsInt) + } + return nil + case 133: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return TtsPlatform(rawValue: enumResultAsInt) + } + return nil + case 134: + return Voice.fromList(self.readValue() as! [Any?]) + case 135: + return TtsResult.fromList(self.readValue() as! [Any?]) + case 136: + return TtsProgress.fromList(self.readValue() as! [Any?]) + case 137: + return TtsRateValidRange.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class MessagePigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? FlutterTtsErrorCode { + super.writeByte(129) + super.writeValue(value.rawValue) + } else if let value = value as? IosTextToSpeechAudioCategory { + super.writeByte(130) + super.writeValue(value.rawValue) + } else if let value = value as? IosTextToSpeechAudioMode { + super.writeByte(131) + super.writeValue(value.rawValue) + } else if let value = value as? IosTextToSpeechAudioCategoryOptions { + super.writeByte(132) + super.writeValue(value.rawValue) + } else if let value = value as? TtsPlatform { + super.writeByte(133) + super.writeValue(value.rawValue) + } else if let value = value as? Voice { + super.writeByte(134) + super.writeValue(value.toList()) + } else if let value = value as? TtsResult { + super.writeByte(135) + super.writeValue(value.toList()) + } else if let value = value as? TtsProgress { + super.writeByte(136) + super.writeValue(value.toList()) + } else if let value = value as? TtsRateValidRange { + super.writeByte(137) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class MessagePigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return MessagePigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return MessagePigeonCodecWriter(data: data) + } +} + +class MessagePigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = MessagePigeonCodec(readerWriter: MessagePigeonCodecReaderWriter()) +} + + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol TtsHostApi { + func speak(text: String, forceFocus: Bool, completion: @escaping (Result) -> Void) + func pause(completion: @escaping (Result) -> Void) + func stop(completion: @escaping (Result) -> Void) + func setSpeechRate(rate: Double, completion: @escaping (Result) -> Void) + func setVolume(volume: Double, completion: @escaping (Result) -> Void) + func setPitch(pitch: Double, completion: @escaping (Result) -> Void) + func setVoice(voice: Voice, completion: @escaping (Result) -> Void) + func clearVoice(completion: @escaping (Result) -> Void) + func awaitSpeakCompletion(awaitCompletion: Bool, completion: @escaping (Result) -> Void) + func getLanguages(completion: @escaping (Result<[String], Error>) -> Void) + func getVoices(completion: @escaping (Result<[Voice], Error>) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class TtsHostApiSetup { + static var codec: FlutterStandardMessageCodec { MessagePigeonCodec.shared } + /// Sets up an instance of `TtsHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: TtsHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let speakChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.speak\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + speakChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let textArg = args[0] as! String + let forceFocusArg = args[1] as! Bool + api.speak(text: textArg, forceFocus: forceFocusArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + speakChannel.setMessageHandler(nil) + } + let pauseChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.pause\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + pauseChannel.setMessageHandler { _, reply in + api.pause { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + pauseChannel.setMessageHandler(nil) + } + let stopChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.stop\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + stopChannel.setMessageHandler { _, reply in + api.stop { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + stopChannel.setMessageHandler(nil) + } + let setSpeechRateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.setSpeechRate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setSpeechRateChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let rateArg = args[0] as! Double + api.setSpeechRate(rate: rateArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setSpeechRateChannel.setMessageHandler(nil) + } + let setVolumeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.setVolume\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setVolumeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let volumeArg = args[0] as! Double + api.setVolume(volume: volumeArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setVolumeChannel.setMessageHandler(nil) + } + let setPitchChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.setPitch\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setPitchChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let pitchArg = args[0] as! Double + api.setPitch(pitch: pitchArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setPitchChannel.setMessageHandler(nil) + } + let setVoiceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.setVoice\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setVoiceChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let voiceArg = args[0] as! Voice + api.setVoice(voice: voiceArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setVoiceChannel.setMessageHandler(nil) + } + let clearVoiceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.clearVoice\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + clearVoiceChannel.setMessageHandler { _, reply in + api.clearVoice { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + clearVoiceChannel.setMessageHandler(nil) + } + let awaitSpeakCompletionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.awaitSpeakCompletion\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + awaitSpeakCompletionChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let awaitCompletionArg = args[0] as! Bool + api.awaitSpeakCompletion(awaitCompletion: awaitCompletionArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + awaitSpeakCompletionChannel.setMessageHandler(nil) + } + let getLanguagesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.getLanguages\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getLanguagesChannel.setMessageHandler { _, reply in + api.getLanguages { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getLanguagesChannel.setMessageHandler(nil) + } + let getVoicesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.TtsHostApi.getVoices\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getVoicesChannel.setMessageHandler { _, reply in + api.getVoices { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getVoicesChannel.setMessageHandler(nil) + } + } +} +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol IosTtsHostApi { + func awaitSynthCompletion(awaitCompletion: Bool, completion: @escaping (Result) -> Void) + func synthesizeToFile(text: String, fileName: String, isFullPath: Bool, completion: @escaping (Result) -> Void) + func setSharedInstance(sharedSession: Bool, completion: @escaping (Result) -> Void) + func autoStopSharedSession(autoStop: Bool, completion: @escaping (Result) -> Void) + func setIosAudioCategory(category: IosTextToSpeechAudioCategory, options: [IosTextToSpeechAudioCategoryOptions], mode: IosTextToSpeechAudioMode, completion: @escaping (Result) -> Void) + func getSpeechRateValidRange(completion: @escaping (Result) -> Void) + func isLanguageAvailable(language: String, completion: @escaping (Result) -> Void) + func setLanguange(language: String, completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class IosTtsHostApiSetup { + static var codec: FlutterStandardMessageCodec { MessagePigeonCodec.shared } + /// Sets up an instance of `IosTtsHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: IosTtsHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let awaitSynthCompletionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.awaitSynthCompletion\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + awaitSynthCompletionChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let awaitCompletionArg = args[0] as! Bool + api.awaitSynthCompletion(awaitCompletion: awaitCompletionArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + awaitSynthCompletionChannel.setMessageHandler(nil) + } + let synthesizeToFileChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.synthesizeToFile\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + synthesizeToFileChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let textArg = args[0] as! String + let fileNameArg = args[1] as! String + let isFullPathArg = args[2] as! Bool + api.synthesizeToFile(text: textArg, fileName: fileNameArg, isFullPath: isFullPathArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + synthesizeToFileChannel.setMessageHandler(nil) + } + let setSharedInstanceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setSharedInstance\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setSharedInstanceChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let sharedSessionArg = args[0] as! Bool + api.setSharedInstance(sharedSession: sharedSessionArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setSharedInstanceChannel.setMessageHandler(nil) + } + let autoStopSharedSessionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.autoStopSharedSession\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + autoStopSharedSessionChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let autoStopArg = args[0] as! Bool + api.autoStopSharedSession(autoStop: autoStopArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + autoStopSharedSessionChannel.setMessageHandler(nil) + } + let setIosAudioCategoryChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setIosAudioCategory\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setIosAudioCategoryChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let categoryArg = args[0] as! IosTextToSpeechAudioCategory + let optionsArg = args[1] as! [IosTextToSpeechAudioCategoryOptions] + let modeArg = args[2] as! IosTextToSpeechAudioMode + api.setIosAudioCategory(category: categoryArg, options: optionsArg, mode: modeArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setIosAudioCategoryChannel.setMessageHandler(nil) + } + let getSpeechRateValidRangeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.getSpeechRateValidRange\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getSpeechRateValidRangeChannel.setMessageHandler { _, reply in + api.getSpeechRateValidRange { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getSpeechRateValidRangeChannel.setMessageHandler(nil) + } + let isLanguageAvailableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.isLanguageAvailable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isLanguageAvailableChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languageArg = args[0] as! String + api.isLanguageAvailable(language: languageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + isLanguageAvailableChannel.setMessageHandler(nil) + } + let setLanguangeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setLanguange\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setLanguangeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languageArg = args[0] as! String + api.setLanguange(language: languageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setLanguangeChannel.setMessageHandler(nil) + } + } +} +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol AndroidTtsHostApi { + func awaitSynthCompletion(awaitCompletion: Bool, completion: @escaping (Result) -> Void) + func getMaxSpeechInputLength(completion: @escaping (Result) -> Void) + func setEngine(engine: String, completion: @escaping (Result) -> Void) + func getEngines(completion: @escaping (Result<[String], Error>) -> Void) + func getDefaultEngine(completion: @escaping (Result) -> Void) + func getDefaultVoice(completion: @escaping (Result) -> Void) + /// [Future] which invokes the platform specific method for synthesizeToFile + func synthesizeToFile(text: String, fileName: String, isFullPath: Bool, completion: @escaping (Result) -> Void) + func isLanguageInstalled(language: String, completion: @escaping (Result) -> Void) + func isLanguageAvailable(language: String, completion: @escaping (Result) -> Void) + func areLanguagesInstalled(languages: [String], completion: @escaping (Result<[String: Bool], Error>) -> Void) + func getSpeechRateValidRange(completion: @escaping (Result) -> Void) + func setSilence(timems: Int64, completion: @escaping (Result) -> Void) + func setQueueMode(queueMode: Int64, completion: @escaping (Result) -> Void) + func setAudioAttributesForNavigation(completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class AndroidTtsHostApiSetup { + static var codec: FlutterStandardMessageCodec { MessagePigeonCodec.shared } + /// Sets up an instance of `AndroidTtsHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: AndroidTtsHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let awaitSynthCompletionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.awaitSynthCompletion\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + awaitSynthCompletionChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let awaitCompletionArg = args[0] as! Bool + api.awaitSynthCompletion(awaitCompletion: awaitCompletionArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + awaitSynthCompletionChannel.setMessageHandler(nil) + } + let getMaxSpeechInputLengthChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getMaxSpeechInputLength\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getMaxSpeechInputLengthChannel.setMessageHandler { _, reply in + api.getMaxSpeechInputLength { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getMaxSpeechInputLengthChannel.setMessageHandler(nil) + } + let setEngineChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setEngine\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setEngineChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let engineArg = args[0] as! String + api.setEngine(engine: engineArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setEngineChannel.setMessageHandler(nil) + } + let getEnginesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getEngines\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getEnginesChannel.setMessageHandler { _, reply in + api.getEngines { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getEnginesChannel.setMessageHandler(nil) + } + let getDefaultEngineChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getDefaultEngine\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getDefaultEngineChannel.setMessageHandler { _, reply in + api.getDefaultEngine { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getDefaultEngineChannel.setMessageHandler(nil) + } + let getDefaultVoiceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getDefaultVoice\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getDefaultVoiceChannel.setMessageHandler { _, reply in + api.getDefaultVoice { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getDefaultVoiceChannel.setMessageHandler(nil) + } + /// [Future] which invokes the platform specific method for synthesizeToFile + let synthesizeToFileChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.synthesizeToFile\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + synthesizeToFileChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let textArg = args[0] as! String + let fileNameArg = args[1] as! String + let isFullPathArg = args[2] as! Bool + api.synthesizeToFile(text: textArg, fileName: fileNameArg, isFullPath: isFullPathArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + synthesizeToFileChannel.setMessageHandler(nil) + } + let isLanguageInstalledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.isLanguageInstalled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isLanguageInstalledChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languageArg = args[0] as! String + api.isLanguageInstalled(language: languageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + isLanguageInstalledChannel.setMessageHandler(nil) + } + let isLanguageAvailableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.isLanguageAvailable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isLanguageAvailableChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languageArg = args[0] as! String + api.isLanguageAvailable(language: languageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + isLanguageAvailableChannel.setMessageHandler(nil) + } + let areLanguagesInstalledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.areLanguagesInstalled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + areLanguagesInstalledChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languagesArg = args[0] as! [String] + api.areLanguagesInstalled(languages: languagesArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + areLanguagesInstalledChannel.setMessageHandler(nil) + } + let getSpeechRateValidRangeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getSpeechRateValidRange\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getSpeechRateValidRangeChannel.setMessageHandler { _, reply in + api.getSpeechRateValidRange { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getSpeechRateValidRangeChannel.setMessageHandler(nil) + } + let setSilenceChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setSilence\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setSilenceChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let timemsArg = args[0] as! Int64 + api.setSilence(timems: timemsArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setSilenceChannel.setMessageHandler(nil) + } + let setQueueModeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setQueueMode\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setQueueModeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let queueModeArg = args[0] as! Int64 + api.setQueueMode(queueMode: queueModeArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setQueueModeChannel.setMessageHandler(nil) + } + let setAudioAttributesForNavigationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setAudioAttributesForNavigation\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setAudioAttributesForNavigationChannel.setMessageHandler { _, reply in + api.setAudioAttributesForNavigation { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setAudioAttributesForNavigationChannel.setMessageHandler(nil) + } + } +} +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol MacosTtsHostApi { + func awaitSynthCompletion(awaitCompletion: Bool, completion: @escaping (Result) -> Void) + func getSpeechRateValidRange(completion: @escaping (Result) -> Void) + func setLanguange(language: String, completion: @escaping (Result) -> Void) + func isLanguageAvailable(language: String, completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class MacosTtsHostApiSetup { + static var codec: FlutterStandardMessageCodec { MessagePigeonCodec.shared } + /// Sets up an instance of `MacosTtsHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: MacosTtsHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let awaitSynthCompletionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.awaitSynthCompletion\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + awaitSynthCompletionChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let awaitCompletionArg = args[0] as! Bool + api.awaitSynthCompletion(awaitCompletion: awaitCompletionArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + awaitSynthCompletionChannel.setMessageHandler(nil) + } + let getSpeechRateValidRangeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.getSpeechRateValidRange\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getSpeechRateValidRangeChannel.setMessageHandler { _, reply in + api.getSpeechRateValidRange { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getSpeechRateValidRangeChannel.setMessageHandler(nil) + } + let setLanguangeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.setLanguange\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setLanguangeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languageArg = args[0] as! String + api.setLanguange(language: languageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setLanguangeChannel.setMessageHandler(nil) + } + let isLanguageAvailableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.isLanguageAvailable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isLanguageAvailableChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let languageArg = args[0] as! String + api.isLanguageAvailable(language: languageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + isLanguageAvailableChannel.setMessageHandler(nil) + } + } +} +/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. +protocol TtsFlutterApiProtocol { + func onSpeakStartCb(completion: @escaping (Result) -> Void) + func onSpeakCompleteCb(completion: @escaping (Result) -> Void) + func onSpeakPauseCb(completion: @escaping (Result) -> Void) + func onSpeakResumeCb(completion: @escaping (Result) -> Void) + func onSpeakCancelCb(completion: @escaping (Result) -> Void) + func onSpeakProgressCb(progress progressArg: TtsProgress, completion: @escaping (Result) -> Void) + func onSpeakErrorCb(error errorArg: String, completion: @escaping (Result) -> Void) + func onSynthStartCb(completion: @escaping (Result) -> Void) + func onSynthCompleteCb(completion: @escaping (Result) -> Void) + func onSynthErrorCb(error errorArg: String, completion: @escaping (Result) -> Void) +} +class TtsFlutterApi: TtsFlutterApiProtocol { + private let binaryMessenger: FlutterBinaryMessenger + private let messageChannelSuffix: String + init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") { + self.binaryMessenger = binaryMessenger + self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + } + var codec: MessagePigeonCodec { + return MessagePigeonCodec.shared + } + func onSpeakStartCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakStartCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSpeakCompleteCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakCompleteCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSpeakPauseCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakPauseCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSpeakResumeCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakResumeCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSpeakCancelCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakCancelCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSpeakProgressCb(progress progressArg: TtsProgress, completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakProgressCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([progressArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSpeakErrorCb(error errorArg: String, completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakErrorCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([errorArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSynthStartCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthStartCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSynthCompleteCb(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthCompleteCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + func onSynthErrorCb(error errorArg: String, completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthErrorCb\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([errorArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } +} diff --git a/pigeons/messages.dart b/pigeons/messages.dart new file mode 100644 index 00000000..b9d4507b --- /dev/null +++ b/pigeons/messages.dart @@ -0,0 +1,513 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + dartOptions: DartOptions(), + cppHeaderOut: 'windows/messages.g.h', + cppSourceOut: 'windows/messages.g.cpp', + cppOptions: CppOptions(namespace: 'flutter_tts'), + dartPackageName: 'flutter_tts', + kotlinOut: + 'android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt', + kotlinOptions: KotlinOptions(package: "com.tundralabs.fluttertts"), + swiftOut: "macos/Classes/message.g.swift", + ), +) +enum FlutterTtsErrorCode { + /// general error code for TTS engine not available. + ttsNotAvailable, + + /// The TTS engine failed to initialize in n second. + /// 1 second is the default timeout. + /// e.g. Some Android custom ROMS may trim TTS service, + /// and third party TTS engine may fail to initialize due to battery optimization. + ttsInitTimeout, + + /// not supported on current os version + notSupportedOSVersion, +} + +/// Audio session category identifiers for iOS. +/// +/// See also: +/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/category +enum IosTextToSpeechAudioCategory { + /// The default audio session category. + /// + /// Your audio is silenced by screen locking and by the Silent switch. + /// + /// By default, using this category implies that your app’s audio + /// is nonmixable—activating your session will interrupt + /// any other audio sessions which are also nonmixable. + /// To allow mixing, use the [ambient] category instead. + ambientSolo, + + /// The category for an app in which sound playback is nonprimary — that is, + /// your app also works with the sound turned off. + /// + /// This category is also appropriate for “play-along” apps, + /// such as a virtual piano that a user plays while the Music app is playing. + /// When you use this category, audio from other apps mixes with your audio. + /// Screen locking and the Silent switch (on iPhone, the Ring/Silent switch) silence your audio. + ambient, + + /// The category for playing recorded music or other sounds + /// that are central to the successful use of your app. + /// + /// When using this category, your app audio continues + /// with the Silent switch set to silent or when the screen locks. + /// + /// By default, using this category implies that your app’s audio + /// is nonmixable—activating your session will interrupt + /// any other audio sessions which are also nonmixable. + /// To allow mixing for this category, use the + /// [IosTextToSpeechAudioCategoryOptions.mixWithOthers] option. + playback, + + /// The category for recording (input) and playback (output) of audio, + /// such as for a Voice over Internet Protocol (VoIP) app. + /// + /// Your audio continues with the Silent switch set to silent and with the screen locked. + /// This category is appropriate for simultaneous recording and playback, + /// and also for apps that record and play back, but not simultaneously. + playAndRecord, +} + +/// Audio session mode identifiers for iOS. +/// +/// See also: +/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/mode +enum IosTextToSpeechAudioMode { + /// The default audio session mode. + /// + /// You can use this mode with every [IosTextToSpeechAudioCategory]. + defaultMode, + + /// A mode that the GameKit framework sets on behalf of an application + /// that uses GameKit’s voice chat service. + /// + /// This mode is valid only with the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// + /// Don’t set this mode directly. If you need similar behavior and aren’t + /// using a `GKVoiceChat` object, use [voiceChat] or [videoChat] instead. + gameChat, + + /// A mode that indicates that your app is performing measurement of audio input or output. + /// + /// Use this mode for apps that need to minimize the amount of + /// system-supplied signal processing to input and output signals. + /// If recording on devices with more than one built-in microphone, + /// the session uses the primary microphone. + /// + /// For use with the [IosTextToSpeechAudioCategory.playback] or + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// + /// **Important:** This mode disables some dynamics processing on input and output signals, + /// resulting in a lower-output playback level. + measurement, + + /// A mode that indicates that your app is playing back movie content. + /// + /// When you set this mode, the audio session uses signal processing to enhance + /// movie playback for certain audio routes such as built-in speaker or headphones. + /// You may only use this mode with the + /// [IosTextToSpeechAudioCategory.playback] category. + moviePlayback, + + /// A mode used for continuous spoken audio to pause the audio when another app plays a short audio prompt. + /// + /// This mode is appropriate for apps that play continuous spoken audio, + /// such as podcasts or audio books. Setting this mode indicates that your app + /// should pause, rather than duck, its audio if another app plays + /// a spoken audio prompt. After the interrupting app’s audio ends, you can + /// resume your app’s audio playback. + spokenAudio, + + /// A mode that indicates that your app is engaging in online video conferencing. + /// + /// Use this mode for video chat apps that use the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// When you set this mode, the audio session optimizes the device’s tonal + /// equalization for voice. It also reduces the set of allowable audio routes + /// to only those appropriate for video chat. + /// + /// Using this mode has the side effect of enabling the + /// [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. + videoChat, + + /// A mode that indicates that your app is recording a movie. + /// + /// This mode is valid only with the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// On devices with more than one built-in microphone, + /// the audio session uses the microphone closest to the video camera. + /// + /// Use this mode to ensure that the system provides appropriate audio-signal processing. + videoRecording, + + /// A mode that indicates that your app is performing two-way voice communication, + /// such as using Voice over Internet Protocol (VoIP). + /// + /// Use this mode for Voice over IP (VoIP) apps that use the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// When you set this mode, the session optimizes the device’s tonal + /// equalization for voice and reduces the set of allowable audio routes + /// to only those appropriate for voice chat. + /// + /// Using this mode has the side effect of enabling the + /// [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. + voiceChat, + + /// A mode that indicates that your app plays audio using text-to-speech. + /// + /// Setting this mode allows for different routing behaviors when your app + /// is connected to certain audio devices, such as CarPlay. + /// An example of an app that uses this mode is a turn-by-turn navigation app + /// that plays short prompts to the user. + /// + /// Typically, apps of the same type also configure their sessions to use the + /// [IosTextToSpeechAudioCategoryOptions.duckOthers] and + /// [IosTextToSpeechAudioCategoryOptions.interruptSpokenAudioAndMixWithOthers] options. + voicePrompt, +} + +/// Audio session category options for iOS. +/// +/// See also: +/// * https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions +enum IosTextToSpeechAudioCategoryOptions { + /// An option that indicates whether audio from this session mixes with audio + /// from active sessions in other audio apps. + /// + /// You can set this option explicitly only if the audio session category + /// is [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// If you set the audio session category to [IosTextToSpeechAudioCategory.ambient], + /// the session automatically sets this option. + /// Likewise, setting the [duckOthers] or [interruptSpokenAudioAndMixWithOthers] + /// options also enables this option. + /// + /// If you set this option, your app mixes its audio with audio playing + /// in background apps, such as the Music app. + mixWithOthers, + + /// An option that reduces the volume of other audio sessions while audio + /// from this session plays. + /// + /// You can set this option only if the audio session category is + /// [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// Setting it implicitly sets the [mixWithOthers] option. + /// + /// Use this option to mix your app’s audio with that of others. + /// While your app plays its audio, the system reduces the volume of other + /// audio sessions to make yours more prominent. If your app provides + /// occasional spoken audio, such as in a turn-by-turn navigation app + /// or an exercise app, you should also set the [interruptSpokenAudioAndMixWithOthers] option. + /// + /// Note that ducking begins when you activate your app’s audio session + /// and ends when you deactivate the session. + /// + /// See also: + /// * [FlutterTts.setSharedInstance] + duckOthers, + + /// An option that determines whether to pause spoken audio content + /// from other sessions when your app plays its audio. + /// + /// You can set this option only if the audio session category is + /// [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// Setting this option also sets [mixWithOthers]. + /// + /// If you set this option, the system mixes your audio with other + /// audio sessions, but interrupts (and stops) audio sessions that use the + /// [IosTextToSpeechAudioMode.spokenAudio] audio session mode. + /// It pauses the audio from other apps as long as your session is active. + /// After your audio session deactivates, the system resumes the interrupted app’s audio. + /// + /// Set this option if your app’s audio is occasional and spoken, + /// such as in a turn-by-turn navigation app or an exercise app. + /// This avoids intelligibility problems when two spoken audio apps mix. + /// If you set this option, also set the [duckOthers] option unless + /// you have a specific reason not to. Ducking other audio, rather than + /// interrupting it, is appropriate when the other audio isn’t spoken audio. + interruptSpokenAudioAndMixWithOthers, + + /// An option that determines whether Bluetooth hands-free devices appear + /// as available input routes. + /// + /// You can set this option only if the audio session category is + /// [IosTextToSpeechAudioCategory.playAndRecord] or + /// [IosTextToSpeechAudioCategory.playback]. + /// + /// You’re required to set this option to allow routing audio input and output + /// to a paired Bluetooth Hands-Free Profile (HFP) device. + /// If you clear this option, paired Bluetooth HFP devices don’t show up + /// as available audio input routes. + allowBluetooth, + + /// An option that determines whether you can stream audio from this session + /// to Bluetooth devices that support the Advanced Audio Distribution Profile (A2DP). + /// + /// A2DP is a stereo, output-only profile intended for higher bandwidth + /// audio use cases, such as music playback. + /// The system automatically routes to A2DP ports if you configure an + /// app’s audio session to use the [IosTextToSpeechAudioCategory.ambient], + /// [IosTextToSpeechAudioCategory.ambientSolo], or + /// [IosTextToSpeechAudioCategory.playback] categories. + /// + /// Starting with iOS 10.0, apps using the + /// [IosTextToSpeechAudioCategory.playAndRecord] category may also allow + /// routing output to paired Bluetooth A2DP devices. To enable this behavior, + /// pass this category option when setting your audio session’s category. + /// + /// Note: If this option and the [allowBluetooth] option are both set, + /// when a single device supports both the Hands-Free Profile (HFP) and A2DP, + /// the system gives hands-free ports a higher priority for routing. + allowBluetoothA2DP, + + /// An option that determines whether you can stream audio + /// from this session to AirPlay devices. + /// + /// Setting this option enables the audio session to route audio output + /// to AirPlay devices. You can only explicitly set this option if the + /// audio session’s category is set to [IosTextToSpeechAudioCategory.playAndRecord]. + /// For most other audio session categories, the system sets this option implicitly. + allowAirPlay, + + /// An option that determines whether audio from the session defaults to the built-in speaker instead of the receiver. + /// + /// You can set this option only when using the + /// [IosTextToSpeechAudioCategory.playAndRecord] category. + /// It’s used to modify the category’s routing behavior so that audio + /// is always routed to the speaker rather than the receiver if + /// no other accessories, such as headphones, are in use. + /// + /// When using this option, the system honors user gestures. + /// For example, plugging in a headset causes the route to change to + /// headset mic/headphones, and unplugging the headset causes the route + /// to change to built-in mic/speaker (as opposed to built-in mic/receiver) + /// when you’ve set this override. + /// + /// In the case of using a USB input-only accessory, audio input + /// comes from the accessory, and the system routes audio to the headphones, + /// if attached, or to the speaker if the headphones aren’t plugged in. + /// The use case is to route audio to the speaker instead of the receiver + /// in cases where the audio would normally go to the receiver. + defaultToSpeaker, +} + +class Voice { + final String name; + final String locale; + String? gender; + String? quality; + String? identifier; + + Voice({ + required this.name, + required this.locale, + this.gender, + this.quality, + this.identifier, + }); +} + +class TtsResult { + final bool success; + final String? message; + + TtsResult({required this.success, this.message}); +} + +class TtsProgress { + final String text; + final int start; + final int end; + final String word; + + TtsProgress({ + required this.text, + required this.start, + required this.end, + required this.word, + }); +} + +enum TtsPlatform { android, ios } + +class TtsRateValidRange { + final double minimum; + final double normal; + final double maximum; + final TtsPlatform platform; + + TtsRateValidRange({ + required this.minimum, + required this.normal, + required this.maximum, + required this.platform, + }); +} + +@HostApi() +abstract class TtsHostApi { + @async + TtsResult speak(String text, bool forceFocus); + + @async + TtsResult pause(); + + @async + TtsResult stop(); + + @async + TtsResult setSpeechRate(double rate); + + @async + TtsResult setVolume(double volume); + + @async + TtsResult setPitch(double pitch); + + @async + TtsResult setVoice(Voice voice); + + @async + TtsResult clearVoice(); + + @async + TtsResult awaitSpeakCompletion(bool awaitCompletion); + + @async + List getLanguages(); + + @async + List getVoices(); +} + +@HostApi() +abstract class IosTtsHostApi extends TtsHostApi { + @async + TtsResult awaitSynthCompletion(bool awaitCompletion); + + @async + TtsResult synthesizeToFile( + String text, + String fileName, [ + bool isFullPath = false, + ]); + + @async + TtsResult setSharedInstance(bool sharedSession); + + @async + TtsResult autoStopSharedSession(bool autoStop); + + @async + TtsResult setIosAudioCategory( + IosTextToSpeechAudioCategory category, + List options, { + IosTextToSpeechAudioMode mode = IosTextToSpeechAudioMode.defaultMode, + }); + + @async + TtsRateValidRange getSpeechRateValidRange(); + + @async + bool isLanguageAvailable(String language); + + @async + TtsResult setLanguange(String language); +} + +@HostApi() +abstract class AndroidTtsHostApi extends TtsHostApi { + @async + TtsResult awaitSynthCompletion(bool awaitCompletion); + + @async + int? getMaxSpeechInputLength(); + + @async + TtsResult setEngine(String engine); + + @async + List getEngines(); + + @async + String? getDefaultEngine(); + + @async + Voice? getDefaultVoice(); + + /// [Future] which invokes the platform specific method for synthesizeToFile + @async + TtsResult synthesizeToFile( + String text, + String fileName, [ + bool isFullPath = false, + ]); + + @async + bool isLanguageInstalled(String language); + + @async + bool isLanguageAvailable(String language); + + @async + Map areLanguagesInstalled(List languages); + + @async + TtsRateValidRange getSpeechRateValidRange(); + + @async + TtsResult setSilence(int timems); + + @async + TtsResult setQueueMode(int queueMode); + + @async + TtsResult setAudioAttributesForNavigation(); +} + +@HostApi() +abstract class MacosTtsHostApi extends TtsHostApi { + @async + TtsResult awaitSynthCompletion(bool awaitCompletion); + + @async + TtsRateValidRange getSpeechRateValidRange(); + + @async + TtsResult setLanguange(String language); + + @async + bool isLanguageAvailable(String language); +} + +@FlutterApi() +abstract class TtsFlutterApi { + void onSpeakStartCb(); + + void onSpeakCompleteCb(); + + void onSpeakPauseCb(); + + void onSpeakResumeCb(); + + void onSpeakCancelCb(); + + void onSpeakProgressCb(TtsProgress progress); + + void onSpeakErrorCb(String error); + + void onSynthStartCb(); + + void onSynthCompleteCb(); + + void onSynthErrorCb(String error); +} diff --git a/pubspec.yaml b/pubspec.yaml index 56346a0c..fed8916a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,24 @@ name: flutter_tts description: A flutter plugin for Text to Speech. This plugin is supported on iOS, macOS, Android, Web, & Windows. -version: 4.2.3 +version: 5.0.0 homepage: https://github.com/dlutton/flutter_tts +environment: + sdk: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" + dependencies: flutter: sdk: flutter flutter_web_plugins: sdk: flutter + multiple_result: ^5.2.0 + plugin_platform_interface: ^2.1.8 + +dev_dependencies: + lints: ^6.0.0 + path: ^1.9.1 + pigeon: ^26.0.5 flutter: plugin: @@ -15,21 +26,21 @@ flutter: android: package: com.tundralabs.fluttertts pluginClass: FlutterTtsPlugin + dartPluginClass: FlutterTtsAndroid + fileName: src/flutter_tts_android.dart ios: pluginClass: FlutterTtsPlugin + dartPluginClass: FlutterTtsIos + fileName: src/flutter_tts_ios.dart macos: pluginClass: FlutterTtsPlugin + dartPluginClass: FlutterTtsMacos + fileName: src/flutter_tts_macos.dart windows: pluginClass: FlutterTtsPlugin supportedVariants: - uwp - win32 web: - pluginClass: FlutterTtsPlugin - fileName: flutter_tts_web.dart - -environment: - sdk: ">=3.4.0 <4.0.0" - flutter: ">=1.22.0" -dev_dependencies: - lints: ^3.0.0 + pluginClass: FlutterTtsWeb + fileName: src/flutter_tts_web.dart diff --git a/tools/generate_pigeons.dart b/tools/generate_pigeons.dart new file mode 100644 index 00000000..5a306a7d --- /dev/null +++ b/tools/generate_pigeons.dart @@ -0,0 +1,60 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +void main() async { + print('generting pigeon code...'); + + final rootDir = Platform.script.resolve('../').toFilePath(); + // 获取当前目录 + print('rootDir: $rootDir'); + + try { + // 执行pigeon命令生成代码 + final result = await Process.run( + 'dart', + ['run', 'pigeon', '--input', p.join(rootDir, 'pigeons', 'messages.dart')], + workingDirectory: rootDir, + runInShell: true, + ); + if (result.exitCode == 0) { + print('\n✅ pigeon code generated successfully!'); + } else { + print('\n❌ pigeon code generation failed!'); + print('${result.stderr}'); + print('${result.stdout}'); + exit(1); + } + + // 确保iOS目录存在 + final iosDir = Directory(p.join(rootDir, 'ios', 'Classes')); + if (!iosDir.existsSync()) { + print('iOS dir not exists: ${iosDir.path}'); + return; + } + + // 确保macOS目录存在 + final macosDir = Directory(p.join(rootDir, 'macos', 'Classes')); + if (!macosDir.existsSync()) { + print('macOS dir not exists: ${macosDir.path}'); + return; + } + + // 为iOS单独生成Swift代码(由于pigeon可能不直接支持同时为多个平台生成Swift) + // 这里通过手动复制生成的Swift文件到iOS目录 + final macosSwiftFile = File( + p.join(rootDir, 'macos', 'Classes', 'message.g.swift'), + ); + await macosSwiftFile.copy( + p.join(rootDir, 'ios', 'Classes', 'message.g.swift'), + ); + + print('\n✅ done generating pigeon code!'); + } catch (e) { + print('\n❌ error when generating pigeon code: $e'); + print( + 'please ensure pigeon dependency is installed: dart pub add pigeon --dev', + ); + exit(1); + } +} diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 81148d51..7070ecab 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -15,7 +15,7 @@ endif() execute_process( COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/nuget.exe install -OutputDirectory ${CMAKE_CURRENT_SOURCE_DIR}/packages Microsoft.Windows.CppWinRT -Version 2.0.210312.4 ) - ARGS install "Microsoft.Windows.CppWinRT" -Version 2.0.210503.1 -ExcludeVersion -OutputDirectory ${CMAKE_BINARY_DIR}/packages) +# ARGS install "Microsoft.Windows.CppWinRT" -Version 2.0.210503.1 -ExcludeVersion -OutputDirectory ${CMAKE_BINARY_DIR}/packages) ################ NuGet install end ################ # This value is used when generating builds using this plugin, so it must @@ -24,6 +24,8 @@ set(PLUGIN_NAME "flutter_tts_plugin") add_library(${PLUGIN_NAME} SHARED "flutter_tts_plugin.cpp" + "messages.g.h" + "messages.g.cpp" ) apply_standard_settings(${PLUGIN_NAME}) set_target_properties(${PLUGIN_NAME} PROPERTIES @@ -32,7 +34,7 @@ target_compile_features(${PLUGIN_NAME} PUBLIC cxx_std_20) target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin mincore.lib) if(MSVC) target_compile_options(${PLUGIN_NAME} PRIVATE "/await") diff --git a/windows/flutter_tts_plugin.cpp b/windows/flutter_tts_plugin.cpp index 6d59088a..f84d95bd 100644 --- a/windows/flutter_tts_plugin.cpp +++ b/windows/flutter_tts_plugin.cpp @@ -1,659 +1,601 @@ -#include "include/flutter_tts/flutter_tts_plugin.h" +#include "include/flutter_tts/flutter_tts_plugin.h" + +#include // This must be included before many other Windows headers. -#include -#include + #include #include #include #include +#include + #include #include #include -typedef std::unique_ptr> FlutterResult; -//typedef flutter::MethodResult* PFlutterResult; +#include "messages.g.h" -std::unique_ptr> methodChannel; +using namespace flutter_tts; -#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP) -#include -#include +// #define FORCE_NON_DESKTOP + +typedef std::function reply)> FlutterResult; + +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP) && \ + !defined(FORCE_NON_DESKTOP) #include +#include +#include using namespace winrt; using namespace Windows::Media::SpeechSynthesis; using namespace Concurrency; using namespace std::chrono_literals; -#include #include -namespace { - class FlutterTtsPlugin : public flutter::Plugin { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); - FlutterTtsPlugin(); - virtual ~FlutterTtsPlugin(); - private: - // Called when a method is called on this plugin's channel from Dart. - void HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result); - void speak(const std::string, FlutterResult); - void pause(); - void continuePlay(); - void stop(); - void setVolume(const double); - void setPitch(const double); - void setRate(const double); - void getVoices(flutter::EncodableList&); - void setVoice(const std::string, const std::string, FlutterResult&); - void getLanguages(flutter::EncodableList&); - void setLanguage(const std::string, FlutterResult&); - void addMplayer(); - winrt::Windows::Foundation::IAsyncAction asyncSpeak(const std::string); - bool speaking(); - bool paused(); - SpeechSynthesizer synth; - winrt::Windows::Media::Playback::MediaPlayer mPlayer; - bool isPaused; - bool isSpeaking; - bool awaitSpeakCompletion; - FlutterResult speakResult; - }; - - void FlutterTtsPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarWindows* registrar) { - methodChannel = - std::make_unique>( - registrar->messenger(), "flutter_tts", - &flutter::StandardMethodCodec::GetInstance()); - auto plugin = std::make_unique(); - - methodChannel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto& call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - registrar->AddPlugin(std::move(plugin)); - } - - void FlutterTtsPlugin::addMplayer() { - mPlayer = winrt::Windows::Media::Playback::MediaPlayer::MediaPlayer(); - auto mEndedToken = - mPlayer.MediaEnded([=](Windows::Media::Playback::MediaPlayer const& sender, - Windows::Foundation::IInspectable const& args) - { - methodChannel->InvokeMethod("speak.onComplete", NULL); - if (awaitSpeakCompletion) { - speakResult->Success(1); - } - isSpeaking = false; - }); - } - - bool FlutterTtsPlugin::speaking() { - return isSpeaking; - } - - bool FlutterTtsPlugin::paused() { - return isPaused; - } - - winrt::Windows::Foundation::IAsyncAction FlutterTtsPlugin::asyncSpeak(const std::string text) { - SpeechSynthesisStream speechStream{ - co_await synth.SynthesizeTextToStreamAsync(to_hstring(text)) - }; - winrt::param::hstring cType = L"Audio"; - winrt::Windows::Media::Core::MediaSource source = - winrt::Windows::Media::Core::MediaSource::CreateFromStream(speechStream, cType); - mPlayer.Source(source); - mPlayer.Play(); - } - - void FlutterTtsPlugin::speak(const std::string text, FlutterResult result) { - isSpeaking = true; - auto my_task{ asyncSpeak(text) }; - methodChannel->InvokeMethod("speak.onStart", NULL); - if (awaitSpeakCompletion) speakResult = std::move(result); - else result->Success(1); - }; - - void FlutterTtsPlugin::pause() { - mPlayer.Pause(); - isPaused = true; - methodChannel->InvokeMethod("speak.onPause", NULL); - } - - void FlutterTtsPlugin::continuePlay() { - mPlayer.Play(); - isPaused = false; - methodChannel->InvokeMethod("speak.onContinue", NULL); - } - - void FlutterTtsPlugin::stop() { - methodChannel->InvokeMethod("speak.onCancel", NULL); - if (awaitSpeakCompletion) { - speakResult->Success(1); - } - - mPlayer.Close(); - addMplayer(); - isSpeaking = false; - isPaused = false; - } - void FlutterTtsPlugin::setVolume(const double newVolume) { synth.Options().AudioVolume(newVolume); } - - void FlutterTtsPlugin::setPitch(const double newPitch) { synth.Options().AudioPitch(newPitch); } - - void FlutterTtsPlugin::setRate(const double newRate) { synth.Options().SpeakingRate(newRate + 0.5); } - - void FlutterTtsPlugin::getVoices(flutter::EncodableList& voices) { - auto synthVoices = synth.AllVoices(); - std::for_each(begin(synthVoices), end(synthVoices), [&voices](const VoiceInformation& voice) - { - flutter::EncodableMap voiceInfo; - voiceInfo[flutter::EncodableValue("locale")] = to_string(voice.Language()); - voiceInfo[flutter::EncodableValue("name")] = to_string(voice.DisplayName()); - // Convert VoiceGender to string - std::string gender; - switch (voice.Gender()) { - case VoiceGender::Male: - gender = "male"; - break; - case VoiceGender::Female: - gender = "female"; - break; - default: - gender = "unknown"; - break; - } - voiceInfo[flutter::EncodableValue("gender")] = gender; - // Identifier example "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_MarkM" - voiceInfo[flutter::EncodableValue("identifier")] = to_string(voice.Id()); - voices.push_back(flutter::EncodableMap(voiceInfo)); - }); - } - - void FlutterTtsPlugin::setVoice(const std::string voiceLanguage, const std::string voiceName, FlutterResult& result) { - bool found = false; - auto voices = synth.AllVoices(); - VoiceInformation newVoice = synth.Voice(); - std::for_each(begin(voices), end(voices), [&voiceLanguage, &voiceName, &found, &newVoice](const VoiceInformation& voice) - { - if (to_string(voice.Language()) == voiceLanguage && to_string(voice.DisplayName()) == voiceName) - { - newVoice = voice; - found = true; - } - }); - synth.Voice(newVoice); - if (found) result->Success(1); - else result->Success(0); - } - - void FlutterTtsPlugin::getLanguages(flutter::EncodableList& languages) { - auto synthVoices = synth.AllVoices(); - std::set languagesSet = {}; - std::for_each(begin(synthVoices), end(synthVoices), [&languagesSet](const VoiceInformation& voice) - { - languagesSet.insert(flutter::EncodableValue(to_string(voice.Language()))); - }); - std::for_each(begin(languagesSet), end(languagesSet), [&languages](const flutter::EncodableValue value) - { - languages.push_back(value); - }); - } - void FlutterTtsPlugin::setLanguage(const std::string voiceLanguage, FlutterResult& result) { - bool found = false; - auto voices = synth.AllVoices(); - VoiceInformation newVoice = synth.Voice(); - std::for_each(begin(voices), end(voices), [&voiceLanguage, &newVoice, &found](const VoiceInformation& voice) - { - if (to_string(voice.Language()) == voiceLanguage) newVoice = voice; - found = true; - }); - synth.Voice(newVoice); - if (found) result->Success(1); - else result->Success(0); - } - - - FlutterTtsPlugin::FlutterTtsPlugin() { - synth = SpeechSynthesizer(); - addMplayer(); - isPaused = false; - isSpeaking = false; - awaitSpeakCompletion = false; - speakResult = FlutterResult(); - } - - FlutterTtsPlugin::~FlutterTtsPlugin() { mPlayer.Close(); } - - void FlutterTtsPlugin::HandleMethodCall( - const flutter::MethodCall& method_call, - FlutterResult result) { - if (method_call.method_name().compare("getPlatformVersion") == 0) { - std::ostringstream version_stream; - version_stream << "Windows UWP"; - result->Success(flutter::EncodableValue(version_stream.str())); - } +#include #else -#include + #include -#include #include -#pragma warning(disable:4996) + +#include +#include +#pragma warning(disable : 4996) #include -#pragma warning(default: 4996) -namespace { +#pragma warning(default : 4996) - class FlutterTtsPlugin : public flutter::Plugin { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); - FlutterTtsPlugin(); - virtual ~FlutterTtsPlugin(); - private: - // Called when a method is called on this plugin's channel from Dart. - void HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result); - - void speak(const std::string, FlutterResult); - void pause(); - void continuePlay(); - void stop(); - void setVolume(const double); - void setPitch(const double); - void setRate(const double); - void getVoices(flutter::EncodableList&); - void setVoice(const std::string, const std::string, FlutterResult&); - void getLanguages(flutter::EncodableList&); - void setLanguage(const std::string, FlutterResult&); - - ISpVoice* pVoice; - bool awaitSpeakCompletion = false; - bool isPaused; - double pitch; - bool speaking(); - bool paused(); - FlutterResult speakResult; - HANDLE addWaitHandle; - }; - - void FlutterTtsPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarWindows* registrar) { - methodChannel = - std::make_unique>( - registrar->messenger(), "flutter_tts", - &flutter::StandardMethodCodec::GetInstance()); - auto plugin = std::make_unique(); - methodChannel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto& call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - - registrar->AddPlugin(std::move(plugin)); - } - - FlutterTtsPlugin::FlutterTtsPlugin() { - addWaitHandle = NULL; - isPaused = false; - speakResult = NULL; - pVoice = NULL; - HRESULT hr; - hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); - if (FAILED(hr)) - { - throw std::exception("TTS init failed"); - } - - hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void**)&pVoice); - if (FAILED(hr)) - { - throw std::exception("TTS create instance failed"); - } - pitch = 0; - } - - FlutterTtsPlugin::~FlutterTtsPlugin() { - ::CoUninitialize(); - } - - void CALLBACK setResult(PVOID lpParam, BOOLEAN TimerOrWaitFired) - { - flutter::MethodResult* p = (flutter::MethodResult*) lpParam; - p->Success(1); - } +#endif - void CALLBACK onCompletion(PVOID lpParam, BOOLEAN TimerOrWaitFired) - { - methodChannel->InvokeMethod("speak.onComplete", NULL); - } +namespace { +class FlutterTtsPlugin : public flutter::Plugin, TtsHostApi { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); + FlutterTtsPlugin(flutter::BinaryMessenger* binary_messenger); + virtual ~FlutterTtsPlugin(); + + // override TTSHostApi + virtual void Speak( + const std::string& text, bool force_focus, + std::function reply)> result) override; + virtual void Pause( + std::function reply)> result) override; + virtual void Stop( + std::function reply)> result) override; + virtual void SetSpeechRate( + double rate, + std::function reply)> result) override; + virtual void SetVolume( + double volume, + std::function reply)> result) override; + virtual void SetPitch( + double pitch, + std::function reply)> result) override; + virtual void SetVoice( + const Voice& voice, + std::function reply)> result) override; + virtual void ClearVoice( + std::function reply)> result) override; + virtual void AwaitSpeakCompletion( + bool await_completion, + std::function reply)> result) override; + virtual void GetLanguages( + std::function reply)> result) + override; + virtual void GetVoices( + std::function reply)> result) + override; + +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP) && \ + !defined(FORCE_NON_DESKTOP) + private: + void speak(const std::string, FlutterResult); + void pause(); + void continuePlay(); + void stop(); + void setVolume(const double); + void setPitch(const double); + void setRate(const double); + void getVoices(flutter::EncodableList&); + void setVoice(const std::string&, const std::string&, FlutterResult&); + void getLanguages(flutter::EncodableList&); + void addMplayer(); + winrt::Windows::Foundation::IAsyncAction asyncSpeak(const std::string); + bool speaking(); + bool paused(); + + SpeechSynthesizer synth; + winrt::Windows::Media::Playback::MediaPlayer mPlayer; + bool isPaused; + bool isSpeaking; + bool awaitSpeakCompletion; + FlutterResult speakResult; + TtsFlutterApi flutterApi; - bool FlutterTtsPlugin::speaking() - { - SPVOICESTATUS status; - pVoice->GetStatus(&status, NULL); - if (status.dwRunningState == SPRS_IS_SPEAKING) return true; - return false; - } - bool FlutterTtsPlugin::paused() { return isPaused; } - - - void FlutterTtsPlugin::speak(const std::string text, FlutterResult result) { - HRESULT hr; - const std::string arg = "" + text; - - int wchars_num = MultiByteToWideChar(CP_UTF8, 0, arg.c_str(), -1, NULL, 0); - wchar_t* wstr = new wchar_t[wchars_num]; - MultiByteToWideChar(CP_UTF8, 0, arg.c_str(), -1, wstr, wchars_num); - hr = pVoice->Speak(wstr, 1, NULL); - delete[] wstr; - HANDLE speakCompletionHandle = pVoice->SpeakCompleteEvent(); - methodChannel->InvokeMethod("speak.onStart", NULL); - RegisterWaitForSingleObject(&addWaitHandle, speakCompletionHandle, (WAITORTIMERCALLBACK)&onCompletion, speakResult.get(), INFINITE, WT_EXECUTEONLYONCE); - if (awaitSpeakCompletion){ - speakResult = std::move(result); - RegisterWaitForSingleObject(&addWaitHandle, speakCompletionHandle, (WAITORTIMERCALLBACK)&setResult, speakResult.get(), INFINITE, WT_EXECUTEONLYONCE); - } - else result->Success(1); - } - void FlutterTtsPlugin::pause() - { - if (isPaused == false) - { - pVoice->Pause(); - isPaused = true; - } - methodChannel->InvokeMethod("speak.onPause", NULL); - } - void FlutterTtsPlugin::continuePlay() - { - isPaused = false; - pVoice->Resume(); - methodChannel->InvokeMethod("speak.onContinue", NULL); - } - void FlutterTtsPlugin::stop() - { - pVoice->Speak(L"", 2, NULL); - pVoice->Resume(); - isPaused = false; - methodChannel->InvokeMethod("speak.onCancel", NULL); - } - void FlutterTtsPlugin::setVolume(const double newVolume) - { - const USHORT volume = (short)(100 * newVolume); - pVoice->SetVolume(volume); - } - void FlutterTtsPlugin::setPitch(const double newPitch) {pitch = newPitch;} - void FlutterTtsPlugin::setRate(const double newRate) - { - const long speechRate = (long)((newRate - 0.5) * 15); - pVoice->SetRate(speechRate); - } - void FlutterTtsPlugin::getVoices(flutter::EncodableList& voices) { - HRESULT hr; - IEnumSpObjectTokens* cpEnum = NULL; - hr = SpEnumTokens(SPCAT_VOICES, NULL, NULL, &cpEnum); - if (FAILED(hr)) return; - - ULONG ulCount = 0; - // Get the number of voices. - hr = cpEnum->GetCount(&ulCount); - if (FAILED(hr)) return; - ISpObjectToken* cpVoiceToken = NULL; - while (ulCount--) - { - cpVoiceToken = NULL; - hr = cpEnum->Next(1, &cpVoiceToken, NULL); - if (FAILED(hr)) return; - CComPtr cpAttribKey; - hr = cpVoiceToken->OpenKey(L"Attributes", &cpAttribKey); - if (FAILED(hr)) return; - WCHAR* psz = NULL; - hr = cpAttribKey->GetStringValue(L"Language", &psz); - wchar_t locale[25]; - LCIDToLocaleName((LCID)std::strtol(CW2A(psz), NULL, 16), locale, 25, 0); - ::CoTaskMemFree(psz); - std::string language = CW2A(locale); - psz = NULL; - cpAttribKey->GetStringValue(L"Name", &psz); - std::string name = CW2A(psz); - ::CoTaskMemFree(psz); - flutter::EncodableMap voiceInfo; - voiceInfo[flutter::EncodableValue("locale")] = language; - voiceInfo[flutter::EncodableValue("name")] = name; - voices.push_back(flutter::EncodableMap(voiceInfo)); - cpVoiceToken->Release(); - } - } - void FlutterTtsPlugin::setVoice(const std::string voiceLanguage, const std::string voiceName, FlutterResult& result) { - HRESULT hr; - IEnumSpObjectTokens* cpEnum = NULL; - hr = SpEnumTokens(SPCAT_VOICES, NULL, NULL, &cpEnum); - if (FAILED(hr)) { result->Success(0); return; } - ULONG ulCount = 0; - hr = cpEnum->GetCount(&ulCount); - if (FAILED(hr)) { result->Success(0); return; } - ISpObjectToken* cpVoiceToken = NULL; - bool success = false; - while (ulCount--) - { - cpVoiceToken = NULL; - hr = cpEnum->Next(1, &cpVoiceToken, NULL); - if (FAILED(hr)) { result->Success(0); return; } - CComPtr cpAttribKey; - hr = cpVoiceToken->OpenKey(L"Attributes", &cpAttribKey); - if (FAILED(hr)) { result->Success(0); return; } - WCHAR* psz = NULL; - hr = cpAttribKey->GetStringValue(L"Name", &psz); - if (FAILED(hr)) { result->Success(0); return; } - std::string name = CW2A(psz); - ::CoTaskMemFree(psz); - psz = NULL; - hr = cpAttribKey->GetStringValue(L"Language", &psz); - wchar_t locale[25]; - LCIDToLocaleName((LCID)std::strtol(CW2A(psz), NULL, 16), locale, 25, 0); - ::CoTaskMemFree(psz); - std::string language = CW2A(locale); - if (name == voiceName && language == voiceLanguage) - { - pVoice->SetVoice(cpVoiceToken); - success = true; - } - cpVoiceToken->Release(); - } - result->Success(success ? 1 : 0); - } - void FlutterTtsPlugin::getLanguages(flutter::EncodableList& languages) - { - HRESULT hr; - IEnumSpObjectTokens* cpEnum = NULL; - hr = SpEnumTokens(SPCAT_VOICES, NULL, NULL, &cpEnum); - if (FAILED(hr)) return; - - ULONG ulCount = 0; - // Get the number of voices. - hr = cpEnum->GetCount(&ulCount); - if (FAILED(hr)) return; - ISpObjectToken* cpVoiceToken = NULL; - std::set languagesSet = {}; - while (ulCount--) - { - cpVoiceToken = NULL; - hr = cpEnum->Next(1, &cpVoiceToken, NULL); - if (FAILED(hr)) return; - CComPtr cpAttribKey; - hr = cpVoiceToken->OpenKey(L"Attributes", &cpAttribKey); - if (FAILED(hr)) return; - - WCHAR* psz = NULL; - hr = cpAttribKey->GetStringValue(L"Language", &psz); - wchar_t locale[25]; - LCIDToLocaleName((LCID)std::strtol(CW2A(psz), NULL, 16), locale, 25, 0); - std::string language = CW2A(locale); - languagesSet.insert(flutter::EncodableValue(language)); - ::CoTaskMemFree(psz); - cpVoiceToken->Release(); - } - std::for_each(begin(languagesSet), end(languagesSet), [&languages](const flutter::EncodableValue value) - { - languages.push_back(value); - }); - } - - void FlutterTtsPlugin::setLanguage(const std::string voiceLanguage, FlutterResult& result) { - HRESULT hr; - IEnumSpObjectTokens* cpEnum = NULL; - hr = SpEnumTokens(SPCAT_VOICES, NULL, NULL, &cpEnum); - if (FAILED(hr)) { result->Success(0); return; } - ULONG ulCount = 0; - hr = cpEnum->GetCount(&ulCount); - if (FAILED(hr)) { result->Success(0); return; } - ISpObjectToken* cpVoiceToken = NULL; - bool found = false; - while (ulCount--) - { - cpVoiceToken = NULL; - hr = cpEnum->Next(1, &cpVoiceToken, NULL); - if (FAILED(hr)) { result->Success(0); return; } - CComPtr cpAttribKey; - hr = cpVoiceToken->OpenKey(L"Attributes", &cpAttribKey); - if (FAILED(hr)) { result->Success(0); return; } - - WCHAR* psz = NULL; - hr = cpAttribKey->GetStringValue(L"Language", &psz); - wchar_t locale[25]; - LCIDToLocaleName((LCID)std::strtol(CW2A(psz), NULL, 16), locale, 25, 0); - std::string language = CW2A(locale); - if (language == voiceLanguage) - { - pVoice->SetVoice(cpVoiceToken); - found = true; - } - ::CoTaskMemFree(psz); - cpVoiceToken->Release(); - } - if (found) result->Success(1); - else result->Success(0); - } - - - void FlutterTtsPlugin::HandleMethodCall( - const flutter::MethodCall& method_call, - FlutterResult result) { - - if (method_call.method_name().compare("getPlatformVersion") == 0) { - std::ostringstream version_stream; - version_stream << "Windows "; - if (IsWindows10OrGreater()) { - version_stream << "10+"; - } - else if (IsWindows8OrGreater()) { - version_stream << "8"; - } - else if (IsWindows7OrGreater()) { - version_stream << "7"; - } - result->Success(flutter::EncodableValue(version_stream.str())); - } +#else + + void speak(const std::string, FlutterResult); + void pause(); + void continuePlay(); + void stop(); + void setVolume(const double); + void setPitch(const double); + void setRate(const double); + void getVoices(flutter::EncodableList&); + void setVoice(const std::string, const std::string, FlutterResult&); + void getLanguages(flutter::EncodableList&); + bool speaking(); + bool paused(); + + ISpVoice* pVoice; + bool awaitSpeakCompletion = false; + bool isPaused; + double pitch; + FlutterResult speakResult; + HANDLE addWaitHandle; + + TtsFlutterApi flutterApi; #endif - else if (method_call.method_name().compare("awaitSpeakCompletion") == 0) { - const flutter::EncodableValue arg = method_call.arguments()[0]; - if (std::holds_alternative(arg)) { - awaitSpeakCompletion = std::get(arg); - result->Success(1); - } - else result->Success(0); - } - else if (method_call.method_name().compare("speak") == 0) { - if (isPaused) { continuePlay(); result->Success(1); return; } - const flutter::EncodableValue arg = method_call.arguments()[0]; - if (std::holds_alternative(arg)) { - if (!speaking()) { - const std::string text = std::get(arg); - speak(text, std::move(result)); - } - else result->Success(0); - } - else result->Success(0); - } - else if (method_call.method_name().compare("pause") == 0) { - FlutterTtsPlugin::pause(); - result->Success(1); - } - else if (method_call.method_name().compare("setLanguage") == 0) { - const flutter::EncodableValue arg = method_call.arguments()[0]; - if (std::holds_alternative(arg)) { - const std::string lang = std::get(arg); - setLanguage(lang, result); - } - else result->Success(0); - } - else if (method_call.method_name().compare("setVolume") == 0) { - const flutter::EncodableValue arg = method_call.arguments()[0]; - if (std::holds_alternative(arg)) { - const double newVolume = std::get(arg); - setVolume(newVolume); - result->Success(1); - } - else result->Success(0); - - } - else if (method_call.method_name().compare("setSpeechRate") == 0) { - const flutter::EncodableValue arg = method_call.arguments()[0]; - if (std::holds_alternative(arg)) { - const double newRate = std::get(arg); - setRate(newRate); - result->Success(1); - } - else result->Success(0); - - } - else if (method_call.method_name().compare("setPitch") == 0) { - const flutter::EncodableValue arg = method_call.arguments()[0]; - if (std::holds_alternative(arg)) { - const double newPitch = std::get(arg); - setPitch(newPitch); - result->Success(1); - } - else result->Success(0); +}; + +void FlutterTtsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows* registrar) { + auto plugin = std::make_unique(registrar->messenger()); + TtsHostApi::SetUp(registrar->messenger(), plugin.get()); + registrar->AddPlugin(std::move(plugin)); +} + +void FlutterTtsPlugin::Speak( + const std::string& text, bool force_focus, + std::function reply)> result) { + if (isPaused) { + continuePlay(); + result(std::move(TtsResult(true))); + return; + } + + if (!speaking()) { + speak(text, std::move(result)); + } else { + result(std::move(TtsResult(false))); + } +} + +void FlutterTtsPlugin::Pause( + std::function reply)> result) { + pause(); + result(std::move(TtsResult(true))); +} + +void FlutterTtsPlugin::Stop( + std::function reply)> result) { + stop(); + result(std::move(TtsResult(true))); +} + +void FlutterTtsPlugin::SetSpeechRate( + double rate, std::function reply)> result) { + setRate(rate); + result(std::move(TtsResult(true))); +} + +void FlutterTtsPlugin::SetVolume( + double volume, std::function reply)> result) { + setVolume(volume); + result(std::move(TtsResult(true))); +} + +void FlutterTtsPlugin::SetPitch( + double newPitch, std::function reply)> result) { + setPitch(newPitch); + result(std::move(TtsResult(true))); +} + +void FlutterTtsPlugin::SetVoice( + const Voice& voice, std::function reply)> result) { + setVoice(voice.locale(), voice.name(), result); +} + +void FlutterTtsPlugin::ClearVoice( + std::function reply)> result) { + result(TtsResult(true)); +} + +void FlutterTtsPlugin::AwaitSpeakCompletion( + bool await_completion, + std::function reply)> result) { + awaitSpeakCompletion = await_completion; + result(std::move(TtsResult(true))); +} + +void FlutterTtsPlugin::GetLanguages( + std::function reply)> result) { + flutter::EncodableList l; + getLanguages(l); + result(l); +} + +void FlutterTtsPlugin::GetVoices( + std::function reply)> result) { + flutter::EncodableList l; + getVoices(l); + result(l); +} + +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP) && \ + !defined(FORCE_NON_DESKTOP) + +void FlutterTtsPlugin::addMplayer() { + mPlayer = winrt::Windows::Media::Playback::MediaPlayer::MediaPlayer(); + auto mEndedToken = mPlayer.MediaEnded( + [=](Windows::Media::Playback::MediaPlayer const& sender, + Windows::Foundation::IInspectable const& args) { + flutterApi.OnSpeakCompleteCb([]() {}, [](const FlutterError&) {}); + if (awaitSpeakCompletion) { + speakResult(std::move(TtsResult(true))); } - else if (method_call.method_name().compare("setVoice") == 0) { - const flutter::EncodableValue arg = method_call.arguments()[0]; - if (std::holds_alternative(arg)) { - const flutter::EncodableMap voiceInfo = std::get(arg); - std::string voiceLanguage = ""; - std::string voiceName = ""; - auto voiceLanguage_it = voiceInfo.find(flutter::EncodableValue("locale")); - if (voiceLanguage_it != voiceInfo.end()) voiceLanguage = std::get(voiceLanguage_it->second); - auto voiceName_it = voiceInfo.find(flutter::EncodableValue("name")); - if (voiceName_it != voiceInfo.end()) voiceName = std::get(voiceName_it->second); - setVoice(voiceLanguage, voiceName, result); - } - else result->Success(0); - } - else if (method_call.method_name().compare("stop") == 0) { - stop(); - result->Success(1); - } - else if (method_call.method_name().compare("getLanguages") == 0) { - flutter::EncodableList l; - getLanguages(l); - result->Success(l); - } - else if (method_call.method_name().compare("getVoices") == 0) { - flutter::EncodableList l; - getVoices(l); - result->Success(l); - } - else { - result->NotImplemented(); - } - } + isSpeaking = false; + }); +} + +bool FlutterTtsPlugin::speaking() { return isSpeaking; } + +bool FlutterTtsPlugin::paused() { return isPaused; } + +winrt::Windows::Foundation::IAsyncAction FlutterTtsPlugin::asyncSpeak( + const std::string text) { + SpeechSynthesisStream speechStream{ + co_await synth.SynthesizeTextToStreamAsync(to_hstring(text))}; + winrt::param::hstring cType = L"Audio"; + winrt::Windows::Media::Core::MediaSource source = + winrt::Windows::Media::Core::MediaSource::CreateFromStream(speechStream, + cType); + mPlayer.Source(source); + mPlayer.Play(); +} + +void FlutterTtsPlugin::speak(const std::string text, FlutterResult result) { + isSpeaking = true; + auto my_task{asyncSpeak(text)}; + flutterApi.OnSpeakStartCb([]() {}, [](const FlutterError&) {}); + if (awaitSpeakCompletion) + speakResult = std::move(result); + else { + result(std::move(TtsResult(true))); + // result(std::move(TtsResult(true))); + } +} + +void FlutterTtsPlugin::pause() { + mPlayer.Pause(); + isPaused = true; + flutterApi.OnSpeakPauseCb([]() {}, [](const FlutterError&) {}); +} + +void FlutterTtsPlugin::continuePlay() { + mPlayer.Play(); + isPaused = false; + flutterApi.OnSpeakResumeCb([]() {}, [](const FlutterError&) {}); +} + +void FlutterTtsPlugin::stop() { + flutterApi.OnSpeakCancelCb([]() {}, [](const FlutterError&) {}); + if (awaitSpeakCompletion) { + speakResult(std::move(TtsResult(true))); + } + + mPlayer.Close(); + addMplayer(); + isSpeaking = false; + isPaused = false; +} +void FlutterTtsPlugin::setVolume(const double newVolume) { + synth.Options().AudioVolume(newVolume); +} + +void FlutterTtsPlugin::setPitch(const double newPitch) { + synth.Options().AudioPitch(newPitch); +} + +void FlutterTtsPlugin::setRate(const double newRate) { + synth.Options().SpeakingRate(newRate + 0.5); +} + +void FlutterTtsPlugin::getVoices(flutter::EncodableList& voices) { + auto synthVoices = synth.AllVoices(); + for (auto voice : synthVoices) { + auto voiceInfo = Voice(to_string(voice.DisplayName()), to_string(voice.Language())); + // Convert VoiceGender to string + std::string gender; + switch (voice.Gender()) { + case VoiceGender::Male: + gender = "male"; + break; + case VoiceGender::Female: + gender = "female"; + break; + default: + gender = "unknown"; + break; + } + voiceInfo.set_gender(gender); + // Identifier example + // "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_MarkM" + voiceInfo.set_identifier(to_string(voice.Id())); + voices.push_back(flutter::CustomEncodableValue(voiceInfo)); + } +} + +void FlutterTtsPlugin::setVoice(const std::string& voiceLanguage, + const std::string& voiceName, + FlutterResult& result) { + bool found = false; + auto voices = synth.AllVoices(); + VoiceInformation newVoice = synth.Voice(); + std::for_each(begin(voices), end(voices), + [&voiceLanguage, &voiceName, &found, + &newVoice](const VoiceInformation& voice) { + if (to_string(voice.Language()) == voiceLanguage && + to_string(voice.DisplayName()) == voiceName) { + newVoice = voice; + found = true; + } + }); + synth.Voice(newVoice); + if (found) { + result(std::move(TtsResult(true))); + } else { + result(std::move(TtsResult(false))); + } } +void FlutterTtsPlugin::getLanguages(flutter::EncodableList& languages) { + auto synthVoices = synth.AllVoices(); + std::set languagesSet = {}; + std::for_each(begin(synthVoices), end(synthVoices), + [&languagesSet](const VoiceInformation& voice) { + languagesSet.insert( + flutter::EncodableValue(to_string(voice.Language()))); + }); + std::for_each(begin(languagesSet), end(languagesSet), + [&languages](const flutter::EncodableValue value) { + languages.push_back(value); + }); +} + +FlutterTtsPlugin::FlutterTtsPlugin(flutter::BinaryMessenger* binary_messenger) + : flutterApi(TtsFlutterApi(binary_messenger)) { + synth = SpeechSynthesizer(); + addMplayer(); + isPaused = false; + isSpeaking = false; + awaitSpeakCompletion = false; + speakResult = FlutterResult(); +} + +FlutterTtsPlugin::~FlutterTtsPlugin() { mPlayer.Close(); } +#else + +FlutterTtsPlugin::FlutterTtsPlugin(flutter::BinaryMessenger* binary_messenger) + : flutterApi(TtsFlutterApi(binary_messenger)) { + addWaitHandle = NULL; + isPaused = false; + speakResult = NULL; + pVoice = NULL; + HRESULT hr; + hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + if (FAILED(hr)) { + throw std::exception("TTS init failed"); + } + + hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, + (void**)&pVoice); + if (FAILED(hr)) { + throw std::exception("TTS create instance failed"); + } + pitch = 0; +} + +FlutterTtsPlugin::~FlutterTtsPlugin() { ::CoUninitialize(); } + +void CALLBACK setResult(PVOID lpParam, BOOLEAN TimerOrWaitFired) { + flutter::MethodResult* p = + (flutter::MethodResult*)lpParam; + p->Success(1); +} + +void CALLBACK onCompletion(PVOID lpParam, BOOLEAN TimerOrWaitFired) { + auto thisPointer = static_cast(lpParam); + thisPointer->speakResult(TtsResult(true)); + thisPointer->flutterApi.OnSpeakCompleteCb([]() {}, + [](const FlutterError&) {}); +} + +bool FlutterTtsPlugin::speaking() { + SPVOICESTATUS status; + pVoice->GetStatus(&status, NULL); + if (status.dwRunningState == SPRS_IS_SPEAKING) return true; + return false; +} +bool FlutterTtsPlugin::paused() { return isPaused; } + +void FlutterTtsPlugin::speak(const std::string text, FlutterResult result) { + HRESULT hr; + const std::string arg = + "" + text; + + int wchars_num = MultiByteToWideChar(CP_UTF8, 0, arg.c_str(), -1, NULL, 0); + wchar_t* wstr = new wchar_t[wchars_num]; + MultiByteToWideChar(CP_UTF8, 0, arg.c_str(), -1, wstr, wchars_num); + hr = pVoice->Speak(wstr, 1, NULL); + delete[] wstr; + HANDLE speakCompletionHandle = pVoice->SpeakCompleteEvent(); + flutterApi.OnSpeakStartCb([]() {}, [](const FlutterError&) {}); + RegisterWaitForSingleObject(&addWaitHandle, speakCompletionHandle, + (WAITORTIMERCALLBACK)&onCompletion, this, + INFINITE, WT_EXECUTEONLYONCE); + if (awaitSpeakCompletion) { + speakResult = std::move(result); + RegisterWaitForSingleObject(&addWaitHandle, speakCompletionHandle, + (WAITORTIMERCALLBACK)&setResult, this, INFINITE, + WT_EXECUTEONLYONCE); + } else + result(std::move(TtsResult(true))); +} +void FlutterTtsPlugin::pause() { + if (isPaused == false) { + pVoice->Pause(); + isPaused = true; + } + flutterApi.OnSpeakPauseCb([]() {}, [](const FlutterError&) {}); +} +void FlutterTtsPlugin::continuePlay() { + isPaused = false; + pVoice->Resume(); + flutterApi.OnSpeakResumeCb([]() {}, [](const FlutterError&) {}); +} +void FlutterTtsPlugin::stop() { + pVoice->Speak(L"", 2, NULL); + pVoice->Resume(); + isPaused = false; + flutterApi.OnSpeakCancelCb([]() {}, [](const FlutterError&) {}); +} +void FlutterTtsPlugin::setVolume(const double newVolume) { + const USHORT volume = (short)(100 * newVolume); + pVoice->SetVolume(volume); +} +void FlutterTtsPlugin::setPitch(const double newPitch) { pitch = newPitch; } +void FlutterTtsPlugin::setRate(const double newRate) { + const long speechRate = (long)((newRate - 0.5) * 15); + pVoice->SetRate(speechRate); +} +void FlutterTtsPlugin::getVoices(flutter::EncodableList& voices) { + HRESULT hr; + IEnumSpObjectTokens* cpEnum = NULL; + hr = SpEnumTokens(SPCAT_VOICES, NULL, NULL, &cpEnum); + if (FAILED(hr)) return; + + ULONG ulCount = 0; + // Get the number of voices. + hr = cpEnum->GetCount(&ulCount); + if (FAILED(hr)) return; + ISpObjectToken* cpVoiceToken = NULL; + while (ulCount--) { + cpVoiceToken = NULL; + hr = cpEnum->Next(1, &cpVoiceToken, NULL); + if (FAILED(hr)) return; + CComPtr cpAttribKey; + hr = cpVoiceToken->OpenKey(L"Attributes", &cpAttribKey); + if (FAILED(hr)) return; + WCHAR* psz = NULL; + hr = cpAttribKey->GetStringValue(L"Language", &psz); + wchar_t locale[25]; + LCIDToLocaleName((LCID)std::strtol(CW2A(psz), NULL, 16), locale, 25, 0); + ::CoTaskMemFree(psz); + std::string language = CW2A(locale); + psz = NULL; + cpAttribKey->GetStringValue(L"Name", &psz); + std::string name = CW2A(psz); + ::CoTaskMemFree(psz); + auto voiceInfo = Voice (name, language); + voices.push_back(flutter::CustomEncodableValue(voiceInfo)); + cpVoiceToken->Release(); + } +} +void FlutterTtsPlugin::setVoice(const std::string voiceLanguage, + const std::string voiceName, + FlutterResult& result) { + HRESULT hr; + IEnumSpObjectTokens* cpEnum = NULL; + hr = SpEnumTokens(SPCAT_VOICES, NULL, NULL, &cpEnum); + if (FAILED(hr)) { + result(std::move(TtsResult(false))); + return; + } + ULONG ulCount = 0; + hr = cpEnum->GetCount(&ulCount); + if (FAILED(hr)) { + result(std::move(TtsResult(false))); + return; + } + ISpObjectToken* cpVoiceToken = NULL; + bool success = false; + while (ulCount--) { + cpVoiceToken = NULL; + hr = cpEnum->Next(1, &cpVoiceToken, NULL); + if (FAILED(hr)) { + result(std::move(TtsResult(false))); + return; + } + CComPtr cpAttribKey; + hr = cpVoiceToken->OpenKey(L"Attributes", &cpAttribKey); + if (FAILED(hr)) { + result(std::move(TtsResult(false))); + return; + } + WCHAR* psz = NULL; + hr = cpAttribKey->GetStringValue(L"Name", &psz); + if (FAILED(hr)) { + result(std::move(TtsResult(false))); + return; + } + std::string name = CW2A(psz); + ::CoTaskMemFree(psz); + psz = NULL; + hr = cpAttribKey->GetStringValue(L"Language", &psz); + wchar_t locale[25]; + LCIDToLocaleName((LCID)std::strtol(CW2A(psz), NULL, 16), locale, 25, 0); + ::CoTaskMemFree(psz); + std::string language = CW2A(locale); + if (name == voiceName && language == voiceLanguage) { + pVoice->SetVoice(cpVoiceToken); + success = true; + } + cpVoiceToken->Release(); + } + result(TtsResult(success)); +} +void FlutterTtsPlugin::getLanguages(flutter::EncodableList& languages) { + HRESULT hr; + IEnumSpObjectTokens* cpEnum = NULL; + hr = SpEnumTokens(SPCAT_VOICES, NULL, NULL, &cpEnum); + if (FAILED(hr)) return; + + ULONG ulCount = 0; + // Get the number of voices. + hr = cpEnum->GetCount(&ulCount); + if (FAILED(hr)) return; + ISpObjectToken* cpVoiceToken = NULL; + std::set languagesSet = {}; + while (ulCount--) { + cpVoiceToken = NULL; + hr = cpEnum->Next(1, &cpVoiceToken, NULL); + if (FAILED(hr)) return; + CComPtr cpAttribKey; + hr = cpVoiceToken->OpenKey(L"Attributes", &cpAttribKey); + if (FAILED(hr)) return; + + WCHAR* psz = NULL; + hr = cpAttribKey->GetStringValue(L"Language", &psz); + wchar_t locale[25]; + LCIDToLocaleName((LCID)std::strtol(CW2A(psz), NULL, 16), locale, 25, 0); + std::string language = CW2A(locale); + languagesSet.insert(flutter::EncodableValue(language)); + ::CoTaskMemFree(psz); + cpVoiceToken->Release(); + } + std::for_each(begin(languagesSet), end(languagesSet), + [&languages](const flutter::EncodableValue value) { + languages.push_back(value); + }); +} +#endif + +} // namespace + void FlutterTtsPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar) { - FlutterTtsPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarManager::GetInstance() - ->GetRegistrar(registrar)); + FlutterDesktopPluginRegistrarRef registrar) { + FlutterTtsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); } diff --git a/windows/messages.g.cpp b/windows/messages.g.cpp new file mode 100644 index 00000000..6cee4294 --- /dev/null +++ b/windows/messages.g.cpp @@ -0,0 +1,1848 @@ +// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#undef _HAS_EXCEPTIONS + +#include "messages.g.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace flutter_tts { +using flutter::BasicMessageChannel; +using flutter::CustomEncodableValue; +using flutter::EncodableList; +using flutter::EncodableMap; +using flutter::EncodableValue; + +FlutterError CreateConnectionError(const std::string channel_name) { + return FlutterError( + "channel-error", + "Unable to establish connection on channel: '" + channel_name + "'.", + EncodableValue("")); +} + +// Voice + +Voice::Voice( + const std::string& name, + const std::string& locale) + : name_(name), + locale_(locale) {} + +Voice::Voice( + const std::string& name, + const std::string& locale, + const std::string* gender, + const std::string* quality, + const std::string* identifier) + : name_(name), + locale_(locale), + gender_(gender ? std::optional(*gender) : std::nullopt), + quality_(quality ? std::optional(*quality) : std::nullopt), + identifier_(identifier ? std::optional(*identifier) : std::nullopt) {} + +const std::string& Voice::name() const { + return name_; +} + +void Voice::set_name(std::string_view value_arg) { + name_ = value_arg; +} + + +const std::string& Voice::locale() const { + return locale_; +} + +void Voice::set_locale(std::string_view value_arg) { + locale_ = value_arg; +} + + +const std::string* Voice::gender() const { + return gender_ ? &(*gender_) : nullptr; +} + +void Voice::set_gender(const std::string_view* value_arg) { + gender_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void Voice::set_gender(std::string_view value_arg) { + gender_ = value_arg; +} + + +const std::string* Voice::quality() const { + return quality_ ? &(*quality_) : nullptr; +} + +void Voice::set_quality(const std::string_view* value_arg) { + quality_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void Voice::set_quality(std::string_view value_arg) { + quality_ = value_arg; +} + + +const std::string* Voice::identifier() const { + return identifier_ ? &(*identifier_) : nullptr; +} + +void Voice::set_identifier(const std::string_view* value_arg) { + identifier_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void Voice::set_identifier(std::string_view value_arg) { + identifier_ = value_arg; +} + + +EncodableList Voice::ToEncodableList() const { + EncodableList list; + list.reserve(5); + list.push_back(EncodableValue(name_)); + list.push_back(EncodableValue(locale_)); + list.push_back(gender_ ? EncodableValue(*gender_) : EncodableValue()); + list.push_back(quality_ ? EncodableValue(*quality_) : EncodableValue()); + list.push_back(identifier_ ? EncodableValue(*identifier_) : EncodableValue()); + return list; +} + +Voice Voice::FromEncodableList(const EncodableList& list) { + Voice decoded( + std::get(list[0]), + std::get(list[1])); + auto& encodable_gender = list[2]; + if (!encodable_gender.IsNull()) { + decoded.set_gender(std::get(encodable_gender)); + } + auto& encodable_quality = list[3]; + if (!encodable_quality.IsNull()) { + decoded.set_quality(std::get(encodable_quality)); + } + auto& encodable_identifier = list[4]; + if (!encodable_identifier.IsNull()) { + decoded.set_identifier(std::get(encodable_identifier)); + } + return decoded; +} + +// TtsResult + +TtsResult::TtsResult(bool success) + : success_(success) {} + +TtsResult::TtsResult( + bool success, + const std::string* message) + : success_(success), + message_(message ? std::optional(*message) : std::nullopt) {} + +bool TtsResult::success() const { + return success_; +} + +void TtsResult::set_success(bool value_arg) { + success_ = value_arg; +} + + +const std::string* TtsResult::message() const { + return message_ ? &(*message_) : nullptr; +} + +void TtsResult::set_message(const std::string_view* value_arg) { + message_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void TtsResult::set_message(std::string_view value_arg) { + message_ = value_arg; +} + + +EncodableList TtsResult::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(success_)); + list.push_back(message_ ? EncodableValue(*message_) : EncodableValue()); + return list; +} + +TtsResult TtsResult::FromEncodableList(const EncodableList& list) { + TtsResult decoded( + std::get(list[0])); + auto& encodable_message = list[1]; + if (!encodable_message.IsNull()) { + decoded.set_message(std::get(encodable_message)); + } + return decoded; +} + +// TtsProgress + +TtsProgress::TtsProgress( + const std::string& text, + int64_t start, + int64_t end, + const std::string& word) + : text_(text), + start_(start), + end_(end), + word_(word) {} + +const std::string& TtsProgress::text() const { + return text_; +} + +void TtsProgress::set_text(std::string_view value_arg) { + text_ = value_arg; +} + + +int64_t TtsProgress::start() const { + return start_; +} + +void TtsProgress::set_start(int64_t value_arg) { + start_ = value_arg; +} + + +int64_t TtsProgress::end() const { + return end_; +} + +void TtsProgress::set_end(int64_t value_arg) { + end_ = value_arg; +} + + +const std::string& TtsProgress::word() const { + return word_; +} + +void TtsProgress::set_word(std::string_view value_arg) { + word_ = value_arg; +} + + +EncodableList TtsProgress::ToEncodableList() const { + EncodableList list; + list.reserve(4); + list.push_back(EncodableValue(text_)); + list.push_back(EncodableValue(start_)); + list.push_back(EncodableValue(end_)); + list.push_back(EncodableValue(word_)); + return list; +} + +TtsProgress TtsProgress::FromEncodableList(const EncodableList& list) { + TtsProgress decoded( + std::get(list[0]), + std::get(list[1]), + std::get(list[2]), + std::get(list[3])); + return decoded; +} + +// TtsRateValidRange + +TtsRateValidRange::TtsRateValidRange( + double minimum, + double normal, + double maximum, + const TtsPlatform& platform) + : minimum_(minimum), + normal_(normal), + maximum_(maximum), + platform_(platform) {} + +double TtsRateValidRange::minimum() const { + return minimum_; +} + +void TtsRateValidRange::set_minimum(double value_arg) { + minimum_ = value_arg; +} + + +double TtsRateValidRange::normal() const { + return normal_; +} + +void TtsRateValidRange::set_normal(double value_arg) { + normal_ = value_arg; +} + + +double TtsRateValidRange::maximum() const { + return maximum_; +} + +void TtsRateValidRange::set_maximum(double value_arg) { + maximum_ = value_arg; +} + + +const TtsPlatform& TtsRateValidRange::platform() const { + return platform_; +} + +void TtsRateValidRange::set_platform(const TtsPlatform& value_arg) { + platform_ = value_arg; +} + + +EncodableList TtsRateValidRange::ToEncodableList() const { + EncodableList list; + list.reserve(4); + list.push_back(EncodableValue(minimum_)); + list.push_back(EncodableValue(normal_)); + list.push_back(EncodableValue(maximum_)); + list.push_back(CustomEncodableValue(platform_)); + return list; +} + +TtsRateValidRange TtsRateValidRange::FromEncodableList(const EncodableList& list) { + TtsRateValidRange decoded( + std::get(list[0]), + std::get(list[1]), + std::get(list[2]), + std::any_cast(std::get(list[3]))); + return decoded; +} + + +PigeonInternalCodecSerializer::PigeonInternalCodecSerializer() {} + +EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( + uint8_t type, + flutter::ByteStreamReader* stream) const { + switch (type) { + case 129: { + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); + } + case 130: { + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); + } + case 131: { + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); + } + case 132: { + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); + } + case 133: { + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); + } + case 134: { + return CustomEncodableValue(Voice::FromEncodableList(std::get(ReadValue(stream)))); + } + case 135: { + return CustomEncodableValue(TtsResult::FromEncodableList(std::get(ReadValue(stream)))); + } + case 136: { + return CustomEncodableValue(TtsProgress::FromEncodableList(std::get(ReadValue(stream)))); + } + case 137: { + return CustomEncodableValue(TtsRateValidRange::FromEncodableList(std::get(ReadValue(stream)))); + } + default: + return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + } +} + +void PigeonInternalCodecSerializer::WriteValue( + const EncodableValue& value, + flutter::ByteStreamWriter* stream) const { + if (const CustomEncodableValue* custom_value = std::get_if(&value)) { + if (custom_value->type() == typeid(FlutterTtsErrorCode)) { + stream->WriteByte(129); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(IosTextToSpeechAudioCategory)) { + stream->WriteByte(130); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(IosTextToSpeechAudioMode)) { + stream->WriteByte(131); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(IosTextToSpeechAudioCategoryOptions)) { + stream->WriteByte(132); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(TtsPlatform)) { + stream->WriteByte(133); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(Voice)) { + stream->WriteByte(134); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(TtsResult)) { + stream->WriteByte(135); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(TtsProgress)) { + stream->WriteByte(136); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(TtsRateValidRange)) { + stream->WriteByte(137); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + } + flutter::StandardCodecSerializer::WriteValue(value, stream); +} + +/// The codec used by TtsHostApi. +const flutter::StandardMessageCodec& TtsHostApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance(&PigeonInternalCodecSerializer::GetInstance()); +} + +// Sets up an instance of `TtsHostApi` to handle messages through the `binary_messenger`. +void TtsHostApi::SetUp( + flutter::BinaryMessenger* binary_messenger, + TtsHostApi* api) { + TtsHostApi::SetUp(binary_messenger, api, ""); +} + +void TtsHostApi::SetUp( + flutter::BinaryMessenger* binary_messenger, + TtsHostApi* api, + const std::string& message_channel_suffix) { + const std::string prepended_suffix = message_channel_suffix.length() > 0 ? std::string(".") + message_channel_suffix : ""; + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.speak" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_text_arg = args.at(0); + if (encodable_text_arg.IsNull()) { + reply(WrapError("text_arg unexpectedly null.")); + return; + } + const auto& text_arg = std::get(encodable_text_arg); + const auto& encodable_force_focus_arg = args.at(1); + if (encodable_force_focus_arg.IsNull()) { + reply(WrapError("force_focus_arg unexpectedly null.")); + return; + } + const auto& force_focus_arg = std::get(encodable_force_focus_arg); + api->Speak(text_arg, force_focus_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.pause" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->Pause([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.stop" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->Stop([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.setSpeechRate" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_rate_arg = args.at(0); + if (encodable_rate_arg.IsNull()) { + reply(WrapError("rate_arg unexpectedly null.")); + return; + } + const auto& rate_arg = std::get(encodable_rate_arg); + api->SetSpeechRate(rate_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.setVolume" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_volume_arg = args.at(0); + if (encodable_volume_arg.IsNull()) { + reply(WrapError("volume_arg unexpectedly null.")); + return; + } + const auto& volume_arg = std::get(encodable_volume_arg); + api->SetVolume(volume_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.setPitch" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_pitch_arg = args.at(0); + if (encodable_pitch_arg.IsNull()) { + reply(WrapError("pitch_arg unexpectedly null.")); + return; + } + const auto& pitch_arg = std::get(encodable_pitch_arg); + api->SetPitch(pitch_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.setVoice" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_voice_arg = args.at(0); + if (encodable_voice_arg.IsNull()) { + reply(WrapError("voice_arg unexpectedly null.")); + return; + } + const auto& voice_arg = std::any_cast(std::get(encodable_voice_arg)); + api->SetVoice(voice_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.clearVoice" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->ClearVoice([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.awaitSpeakCompletion" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_await_completion_arg = args.at(0); + if (encodable_await_completion_arg.IsNull()) { + reply(WrapError("await_completion_arg unexpectedly null.")); + return; + } + const auto& await_completion_arg = std::get(encodable_await_completion_arg); + api->AwaitSpeakCompletion(await_completion_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.getLanguages" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->GetLanguages([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.TtsHostApi.getVoices" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->GetVoices([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } +} + +EncodableValue TtsHostApi::WrapError(std::string_view error_message) { + return EncodableValue(EncodableList{ + EncodableValue(std::string(error_message)), + EncodableValue("Error"), + EncodableValue() + }); +} + +EncodableValue TtsHostApi::WrapError(const FlutterError& error) { + return EncodableValue(EncodableList{ + EncodableValue(error.code()), + EncodableValue(error.message()), + error.details() + }); +} + +/// The codec used by IosTtsHostApi. +const flutter::StandardMessageCodec& IosTtsHostApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance(&PigeonInternalCodecSerializer::GetInstance()); +} + +// Sets up an instance of `IosTtsHostApi` to handle messages through the `binary_messenger`. +void IosTtsHostApi::SetUp( + flutter::BinaryMessenger* binary_messenger, + IosTtsHostApi* api) { + IosTtsHostApi::SetUp(binary_messenger, api, ""); +} + +void IosTtsHostApi::SetUp( + flutter::BinaryMessenger* binary_messenger, + IosTtsHostApi* api, + const std::string& message_channel_suffix) { + const std::string prepended_suffix = message_channel_suffix.length() > 0 ? std::string(".") + message_channel_suffix : ""; + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.awaitSynthCompletion" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_await_completion_arg = args.at(0); + if (encodable_await_completion_arg.IsNull()) { + reply(WrapError("await_completion_arg unexpectedly null.")); + return; + } + const auto& await_completion_arg = std::get(encodable_await_completion_arg); + api->AwaitSynthCompletion(await_completion_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.synthesizeToFile" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_text_arg = args.at(0); + if (encodable_text_arg.IsNull()) { + reply(WrapError("text_arg unexpectedly null.")); + return; + } + const auto& text_arg = std::get(encodable_text_arg); + const auto& encodable_file_name_arg = args.at(1); + if (encodable_file_name_arg.IsNull()) { + reply(WrapError("file_name_arg unexpectedly null.")); + return; + } + const auto& file_name_arg = std::get(encodable_file_name_arg); + const auto& encodable_is_full_path_arg = args.at(2); + if (encodable_is_full_path_arg.IsNull()) { + reply(WrapError("is_full_path_arg unexpectedly null.")); + return; + } + const auto& is_full_path_arg = std::get(encodable_is_full_path_arg); + api->SynthesizeToFile(text_arg, file_name_arg, is_full_path_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setSharedInstance" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_shared_session_arg = args.at(0); + if (encodable_shared_session_arg.IsNull()) { + reply(WrapError("shared_session_arg unexpectedly null.")); + return; + } + const auto& shared_session_arg = std::get(encodable_shared_session_arg); + api->SetSharedInstance(shared_session_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.autoStopSharedSession" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_auto_stop_arg = args.at(0); + if (encodable_auto_stop_arg.IsNull()) { + reply(WrapError("auto_stop_arg unexpectedly null.")); + return; + } + const auto& auto_stop_arg = std::get(encodable_auto_stop_arg); + api->AutoStopSharedSession(auto_stop_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setIosAudioCategory" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_category_arg = args.at(0); + if (encodable_category_arg.IsNull()) { + reply(WrapError("category_arg unexpectedly null.")); + return; + } + const auto& category_arg = std::any_cast(std::get(encodable_category_arg)); + const auto& encodable_options_arg = args.at(1); + if (encodable_options_arg.IsNull()) { + reply(WrapError("options_arg unexpectedly null.")); + return; + } + const auto& options_arg = std::get(encodable_options_arg); + const auto& encodable_mode_arg = args.at(2); + if (encodable_mode_arg.IsNull()) { + reply(WrapError("mode_arg unexpectedly null.")); + return; + } + const auto& mode_arg = std::any_cast(std::get(encodable_mode_arg)); + api->SetIosAudioCategory(category_arg, options_arg, mode_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.getSpeechRateValidRange" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->GetSpeechRateValidRange([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.isLanguageAvailable" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_language_arg = args.at(0); + if (encodable_language_arg.IsNull()) { + reply(WrapError("language_arg unexpectedly null.")); + return; + } + const auto& language_arg = std::get(encodable_language_arg); + api->IsLanguageAvailable(language_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.IosTtsHostApi.setLanguange" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_language_arg = args.at(0); + if (encodable_language_arg.IsNull()) { + reply(WrapError("language_arg unexpectedly null.")); + return; + } + const auto& language_arg = std::get(encodable_language_arg); + api->SetLanguange(language_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } +} + +EncodableValue IosTtsHostApi::WrapError(std::string_view error_message) { + return EncodableValue(EncodableList{ + EncodableValue(std::string(error_message)), + EncodableValue("Error"), + EncodableValue() + }); +} + +EncodableValue IosTtsHostApi::WrapError(const FlutterError& error) { + return EncodableValue(EncodableList{ + EncodableValue(error.code()), + EncodableValue(error.message()), + error.details() + }); +} + +/// The codec used by AndroidTtsHostApi. +const flutter::StandardMessageCodec& AndroidTtsHostApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance(&PigeonInternalCodecSerializer::GetInstance()); +} + +// Sets up an instance of `AndroidTtsHostApi` to handle messages through the `binary_messenger`. +void AndroidTtsHostApi::SetUp( + flutter::BinaryMessenger* binary_messenger, + AndroidTtsHostApi* api) { + AndroidTtsHostApi::SetUp(binary_messenger, api, ""); +} + +void AndroidTtsHostApi::SetUp( + flutter::BinaryMessenger* binary_messenger, + AndroidTtsHostApi* api, + const std::string& message_channel_suffix) { + const std::string prepended_suffix = message_channel_suffix.length() > 0 ? std::string(".") + message_channel_suffix : ""; + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.awaitSynthCompletion" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_await_completion_arg = args.at(0); + if (encodable_await_completion_arg.IsNull()) { + reply(WrapError("await_completion_arg unexpectedly null.")); + return; + } + const auto& await_completion_arg = std::get(encodable_await_completion_arg); + api->AwaitSynthCompletion(await_completion_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getMaxSpeechInputLength" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->GetMaxSpeechInputLength([reply](ErrorOr>&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + auto output_optional = std::move(output).TakeValue(); + if (output_optional) { + wrapped.push_back(EncodableValue(std::move(output_optional).value())); + } else { + wrapped.push_back(EncodableValue()); + } + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setEngine" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_engine_arg = args.at(0); + if (encodable_engine_arg.IsNull()) { + reply(WrapError("engine_arg unexpectedly null.")); + return; + } + const auto& engine_arg = std::get(encodable_engine_arg); + api->SetEngine(engine_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getEngines" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->GetEngines([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getDefaultEngine" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->GetDefaultEngine([reply](ErrorOr>&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + auto output_optional = std::move(output).TakeValue(); + if (output_optional) { + wrapped.push_back(EncodableValue(std::move(output_optional).value())); + } else { + wrapped.push_back(EncodableValue()); + } + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getDefaultVoice" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->GetDefaultVoice([reply](ErrorOr>&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + auto output_optional = std::move(output).TakeValue(); + if (output_optional) { + wrapped.push_back(CustomEncodableValue(std::move(output_optional).value())); + } else { + wrapped.push_back(EncodableValue()); + } + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.synthesizeToFile" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_text_arg = args.at(0); + if (encodable_text_arg.IsNull()) { + reply(WrapError("text_arg unexpectedly null.")); + return; + } + const auto& text_arg = std::get(encodable_text_arg); + const auto& encodable_file_name_arg = args.at(1); + if (encodable_file_name_arg.IsNull()) { + reply(WrapError("file_name_arg unexpectedly null.")); + return; + } + const auto& file_name_arg = std::get(encodable_file_name_arg); + const auto& encodable_is_full_path_arg = args.at(2); + if (encodable_is_full_path_arg.IsNull()) { + reply(WrapError("is_full_path_arg unexpectedly null.")); + return; + } + const auto& is_full_path_arg = std::get(encodable_is_full_path_arg); + api->SynthesizeToFile(text_arg, file_name_arg, is_full_path_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.isLanguageInstalled" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_language_arg = args.at(0); + if (encodable_language_arg.IsNull()) { + reply(WrapError("language_arg unexpectedly null.")); + return; + } + const auto& language_arg = std::get(encodable_language_arg); + api->IsLanguageInstalled(language_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.isLanguageAvailable" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_language_arg = args.at(0); + if (encodable_language_arg.IsNull()) { + reply(WrapError("language_arg unexpectedly null.")); + return; + } + const auto& language_arg = std::get(encodable_language_arg); + api->IsLanguageAvailable(language_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.areLanguagesInstalled" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_languages_arg = args.at(0); + if (encodable_languages_arg.IsNull()) { + reply(WrapError("languages_arg unexpectedly null.")); + return; + } + const auto& languages_arg = std::get(encodable_languages_arg); + api->AreLanguagesInstalled(languages_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.getSpeechRateValidRange" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->GetSpeechRateValidRange([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setSilence" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_timems_arg = args.at(0); + if (encodable_timems_arg.IsNull()) { + reply(WrapError("timems_arg unexpectedly null.")); + return; + } + const int64_t timems_arg = encodable_timems_arg.LongValue(); + api->SetSilence(timems_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setQueueMode" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_queue_mode_arg = args.at(0); + if (encodable_queue_mode_arg.IsNull()) { + reply(WrapError("queue_mode_arg unexpectedly null.")); + return; + } + const int64_t queue_mode_arg = encodable_queue_mode_arg.LongValue(); + api->SetQueueMode(queue_mode_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.AndroidTtsHostApi.setAudioAttributesForNavigation" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->SetAudioAttributesForNavigation([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } +} + +EncodableValue AndroidTtsHostApi::WrapError(std::string_view error_message) { + return EncodableValue(EncodableList{ + EncodableValue(std::string(error_message)), + EncodableValue("Error"), + EncodableValue() + }); +} + +EncodableValue AndroidTtsHostApi::WrapError(const FlutterError& error) { + return EncodableValue(EncodableList{ + EncodableValue(error.code()), + EncodableValue(error.message()), + error.details() + }); +} + +/// The codec used by MacosTtsHostApi. +const flutter::StandardMessageCodec& MacosTtsHostApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance(&PigeonInternalCodecSerializer::GetInstance()); +} + +// Sets up an instance of `MacosTtsHostApi` to handle messages through the `binary_messenger`. +void MacosTtsHostApi::SetUp( + flutter::BinaryMessenger* binary_messenger, + MacosTtsHostApi* api) { + MacosTtsHostApi::SetUp(binary_messenger, api, ""); +} + +void MacosTtsHostApi::SetUp( + flutter::BinaryMessenger* binary_messenger, + MacosTtsHostApi* api, + const std::string& message_channel_suffix) { + const std::string prepended_suffix = message_channel_suffix.length() > 0 ? std::string(".") + message_channel_suffix : ""; + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.awaitSynthCompletion" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_await_completion_arg = args.at(0); + if (encodable_await_completion_arg.IsNull()) { + reply(WrapError("await_completion_arg unexpectedly null.")); + return; + } + const auto& await_completion_arg = std::get(encodable_await_completion_arg); + api->AwaitSynthCompletion(await_completion_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.getSpeechRateValidRange" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->GetSpeechRateValidRange([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.setLanguange" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_language_arg = args.at(0); + if (encodable_language_arg.IsNull()) { + reply(WrapError("language_arg unexpectedly null.")); + return; + } + const auto& language_arg = std::get(encodable_language_arg); + api->SetLanguange(language_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.MacosTtsHostApi.isLanguageAvailable" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_language_arg = args.at(0); + if (encodable_language_arg.IsNull()) { + reply(WrapError("language_arg unexpectedly null.")); + return; + } + const auto& language_arg = std::get(encodable_language_arg); + api->IsLanguageAvailable(language_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } +} + +EncodableValue MacosTtsHostApi::WrapError(std::string_view error_message) { + return EncodableValue(EncodableList{ + EncodableValue(std::string(error_message)), + EncodableValue("Error"), + EncodableValue() + }); +} + +EncodableValue MacosTtsHostApi::WrapError(const FlutterError& error) { + return EncodableValue(EncodableList{ + EncodableValue(error.code()), + EncodableValue(error.message()), + error.details() + }); +} + +// Generated class from Pigeon that represents Flutter messages that can be called from C++. +TtsFlutterApi::TtsFlutterApi(flutter::BinaryMessenger* binary_messenger) + : binary_messenger_(binary_messenger), + message_channel_suffix_("") {} + +TtsFlutterApi::TtsFlutterApi( + flutter::BinaryMessenger* binary_messenger, + const std::string& message_channel_suffix) + : binary_messenger_(binary_messenger), + message_channel_suffix_(message_channel_suffix.length() > 0 ? std::string(".") + message_channel_suffix : "") {} + +const flutter::StandardMessageCodec& TtsFlutterApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance(&PigeonInternalCodecSerializer::GetInstance()); +} + +void TtsFlutterApi::OnSpeakStartCb( + std::function&& on_success, + std::function&& on_error) { + const std::string channel_name = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakStartCb" + message_channel_suffix_; + BasicMessageChannel<> channel(binary_messenger_, channel_name, &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(); + channel.Send(encoded_api_arguments, [channel_name, on_success = std::move(on_success), on_error = std::move(on_error)](const uint8_t* reply, size_t reply_size) { + std::unique_ptr response = GetCodec().DecodeMessage(reply, reply_size); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError(std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(CreateConnectionError(channel_name)); + } + }); +} + +void TtsFlutterApi::OnSpeakCompleteCb( + std::function&& on_success, + std::function&& on_error) { + const std::string channel_name = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakCompleteCb" + message_channel_suffix_; + BasicMessageChannel<> channel(binary_messenger_, channel_name, &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(); + channel.Send(encoded_api_arguments, [channel_name, on_success = std::move(on_success), on_error = std::move(on_error)](const uint8_t* reply, size_t reply_size) { + std::unique_ptr response = GetCodec().DecodeMessage(reply, reply_size); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError(std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(CreateConnectionError(channel_name)); + } + }); +} + +void TtsFlutterApi::OnSpeakPauseCb( + std::function&& on_success, + std::function&& on_error) { + const std::string channel_name = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakPauseCb" + message_channel_suffix_; + BasicMessageChannel<> channel(binary_messenger_, channel_name, &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(); + channel.Send(encoded_api_arguments, [channel_name, on_success = std::move(on_success), on_error = std::move(on_error)](const uint8_t* reply, size_t reply_size) { + std::unique_ptr response = GetCodec().DecodeMessage(reply, reply_size); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError(std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(CreateConnectionError(channel_name)); + } + }); +} + +void TtsFlutterApi::OnSpeakResumeCb( + std::function&& on_success, + std::function&& on_error) { + const std::string channel_name = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakResumeCb" + message_channel_suffix_; + BasicMessageChannel<> channel(binary_messenger_, channel_name, &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(); + channel.Send(encoded_api_arguments, [channel_name, on_success = std::move(on_success), on_error = std::move(on_error)](const uint8_t* reply, size_t reply_size) { + std::unique_ptr response = GetCodec().DecodeMessage(reply, reply_size); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError(std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(CreateConnectionError(channel_name)); + } + }); +} + +void TtsFlutterApi::OnSpeakCancelCb( + std::function&& on_success, + std::function&& on_error) { + const std::string channel_name = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakCancelCb" + message_channel_suffix_; + BasicMessageChannel<> channel(binary_messenger_, channel_name, &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(); + channel.Send(encoded_api_arguments, [channel_name, on_success = std::move(on_success), on_error = std::move(on_error)](const uint8_t* reply, size_t reply_size) { + std::unique_ptr response = GetCodec().DecodeMessage(reply, reply_size); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError(std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(CreateConnectionError(channel_name)); + } + }); +} + +void TtsFlutterApi::OnSpeakProgressCb( + const TtsProgress& progress_arg, + std::function&& on_success, + std::function&& on_error) { + const std::string channel_name = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakProgressCb" + message_channel_suffix_; + BasicMessageChannel<> channel(binary_messenger_, channel_name, &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(EncodableList{ + CustomEncodableValue(progress_arg), + }); + channel.Send(encoded_api_arguments, [channel_name, on_success = std::move(on_success), on_error = std::move(on_error)](const uint8_t* reply, size_t reply_size) { + std::unique_ptr response = GetCodec().DecodeMessage(reply, reply_size); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError(std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(CreateConnectionError(channel_name)); + } + }); +} + +void TtsFlutterApi::OnSpeakErrorCb( + const std::string& error_arg, + std::function&& on_success, + std::function&& on_error) { + const std::string channel_name = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSpeakErrorCb" + message_channel_suffix_; + BasicMessageChannel<> channel(binary_messenger_, channel_name, &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(EncodableList{ + EncodableValue(error_arg), + }); + channel.Send(encoded_api_arguments, [channel_name, on_success = std::move(on_success), on_error = std::move(on_error)](const uint8_t* reply, size_t reply_size) { + std::unique_ptr response = GetCodec().DecodeMessage(reply, reply_size); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError(std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(CreateConnectionError(channel_name)); + } + }); +} + +void TtsFlutterApi::OnSynthStartCb( + std::function&& on_success, + std::function&& on_error) { + const std::string channel_name = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthStartCb" + message_channel_suffix_; + BasicMessageChannel<> channel(binary_messenger_, channel_name, &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(); + channel.Send(encoded_api_arguments, [channel_name, on_success = std::move(on_success), on_error = std::move(on_error)](const uint8_t* reply, size_t reply_size) { + std::unique_ptr response = GetCodec().DecodeMessage(reply, reply_size); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError(std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(CreateConnectionError(channel_name)); + } + }); +} + +void TtsFlutterApi::OnSynthCompleteCb( + std::function&& on_success, + std::function&& on_error) { + const std::string channel_name = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthCompleteCb" + message_channel_suffix_; + BasicMessageChannel<> channel(binary_messenger_, channel_name, &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(); + channel.Send(encoded_api_arguments, [channel_name, on_success = std::move(on_success), on_error = std::move(on_error)](const uint8_t* reply, size_t reply_size) { + std::unique_ptr response = GetCodec().DecodeMessage(reply, reply_size); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError(std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(CreateConnectionError(channel_name)); + } + }); +} + +void TtsFlutterApi::OnSynthErrorCb( + const std::string& error_arg, + std::function&& on_success, + std::function&& on_error) { + const std::string channel_name = "dev.flutter.pigeon.flutter_tts.TtsFlutterApi.onSynthErrorCb" + message_channel_suffix_; + BasicMessageChannel<> channel(binary_messenger_, channel_name, &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(EncodableList{ + EncodableValue(error_arg), + }); + channel.Send(encoded_api_arguments, [channel_name, on_success = std::move(on_success), on_error = std::move(on_error)](const uint8_t* reply, size_t reply_size) { + std::unique_ptr response = GetCodec().DecodeMessage(reply, reply_size); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError(std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(CreateConnectionError(channel_name)); + } + }); +} + +} // namespace flutter_tts diff --git a/windows/messages.g.h b/windows/messages.g.h new file mode 100644 index 00000000..209c4122 --- /dev/null +++ b/windows/messages.g.h @@ -0,0 +1,740 @@ +// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#ifndef PIGEON_MESSAGES_G_H_ +#define PIGEON_MESSAGES_G_H_ +#include +#include +#include +#include + +#include +#include +#include + +namespace flutter_tts { + + +// Generated class from Pigeon. + +class FlutterError { + public: + explicit FlutterError(const std::string& code) + : code_(code) {} + explicit FlutterError(const std::string& code, const std::string& message) + : code_(code), message_(message) {} + explicit FlutterError(const std::string& code, const std::string& message, const flutter::EncodableValue& details) + : code_(code), message_(message), details_(details) {} + + const std::string& code() const { return code_; } + const std::string& message() const { return message_; } + const flutter::EncodableValue& details() const { return details_; } + + private: + std::string code_; + std::string message_; + flutter::EncodableValue details_; +}; + +template class ErrorOr { + public: + ErrorOr(const T& rhs) : v_(rhs) {} + ErrorOr(const T&& rhs) : v_(std::move(rhs)) {} + ErrorOr(const FlutterError& rhs) : v_(rhs) {} + ErrorOr(const FlutterError&& rhs) : v_(std::move(rhs)) {} + + bool has_error() const { return std::holds_alternative(v_); } + const T& value() const { return std::get(v_); }; + const FlutterError& error() const { return std::get(v_); }; + + private: + friend class TtsHostApi; + friend class IosTtsHostApi; + friend class AndroidTtsHostApi; + friend class MacosTtsHostApi; + friend class TtsFlutterApi; + ErrorOr() = default; + T TakeValue() && { return std::get(std::move(v_)); } + + std::variant v_; +}; + + +enum class FlutterTtsErrorCode { + // general error code for TTS engine not available. + kTtsNotAvailable = 0, + // The TTS engine failed to initialize in n second. + // 1 second is the default timeout. + // e.g. Some Android custom ROMS may trim TTS service, + // and third party TTS engine may fail to initialize due to battery optimization. + kTtsInitTimeout = 1, + // not supported on current os version + kNotSupportedOSVersion = 2 +}; + +// Audio session category identifiers for iOS. +// +// See also: +// * https://developer.apple.com/documentation/avfaudio/avaudiosession/category +enum class IosTextToSpeechAudioCategory { + // The default audio session category. + // + // Your audio is silenced by screen locking and by the Silent switch. + // + // By default, using this category implies that your app’s audio + // is nonmixable—activating your session will interrupt + // any other audio sessions which are also nonmixable. + // To allow mixing, use the [ambient] category instead. + kAmbientSolo = 0, + // The category for an app in which sound playback is nonprimary — that is, + // your app also works with the sound turned off. + // + // This category is also appropriate for “play-along” apps, + // such as a virtual piano that a user plays while the Music app is playing. + // When you use this category, audio from other apps mixes with your audio. + // Screen locking and the Silent switch (on iPhone, the Ring/Silent switch) silence your audio. + kAmbient = 1, + // The category for playing recorded music or other sounds + // that are central to the successful use of your app. + // + // When using this category, your app audio continues + // with the Silent switch set to silent or when the screen locks. + // + // By default, using this category implies that your app’s audio + // is nonmixable—activating your session will interrupt + // any other audio sessions which are also nonmixable. + // To allow mixing for this category, use the + // [IosTextToSpeechAudioCategoryOptions.mixWithOthers] option. + kPlayback = 2, + // The category for recording (input) and playback (output) of audio, + // such as for a Voice over Internet Protocol (VoIP) app. + // + // Your audio continues with the Silent switch set to silent and with the screen locked. + // This category is appropriate for simultaneous recording and playback, + // and also for apps that record and play back, but not simultaneously. + kPlayAndRecord = 3 +}; + +// Audio session mode identifiers for iOS. +// +// See also: +// * https://developer.apple.com/documentation/avfaudio/avaudiosession/mode +enum class IosTextToSpeechAudioMode { + // The default audio session mode. + // + // You can use this mode with every [IosTextToSpeechAudioCategory]. + kDefaultMode = 0, + // A mode that the GameKit framework sets on behalf of an application + // that uses GameKit’s voice chat service. + // + // This mode is valid only with the + // [IosTextToSpeechAudioCategory.playAndRecord] category. + // + // Don’t set this mode directly. If you need similar behavior and aren’t + // using a `GKVoiceChat` object, use [voiceChat] or [videoChat] instead. + kGameChat = 1, + // A mode that indicates that your app is performing measurement of audio input or output. + // + // Use this mode for apps that need to minimize the amount of + // system-supplied signal processing to input and output signals. + // If recording on devices with more than one built-in microphone, + // the session uses the primary microphone. + // + // For use with the [IosTextToSpeechAudioCategory.playback] or + // [IosTextToSpeechAudioCategory.playAndRecord] category. + // + // **Important:** This mode disables some dynamics processing on input and output signals, + // resulting in a lower-output playback level. + kMeasurement = 2, + // A mode that indicates that your app is playing back movie content. + // + // When you set this mode, the audio session uses signal processing to enhance + // movie playback for certain audio routes such as built-in speaker or headphones. + // You may only use this mode with the + // [IosTextToSpeechAudioCategory.playback] category. + kMoviePlayback = 3, + // A mode used for continuous spoken audio to pause the audio when another app plays a short audio prompt. + // + // This mode is appropriate for apps that play continuous spoken audio, + // such as podcasts or audio books. Setting this mode indicates that your app + // should pause, rather than duck, its audio if another app plays + // a spoken audio prompt. After the interrupting app’s audio ends, you can + // resume your app’s audio playback. + kSpokenAudio = 4, + // A mode that indicates that your app is engaging in online video conferencing. + // + // Use this mode for video chat apps that use the + // [IosTextToSpeechAudioCategory.playAndRecord] category. + // When you set this mode, the audio session optimizes the device’s tonal + // equalization for voice. It also reduces the set of allowable audio routes + // to only those appropriate for video chat. + // + // Using this mode has the side effect of enabling the + // [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. + kVideoChat = 5, + // A mode that indicates that your app is recording a movie. + // + // This mode is valid only with the + // [IosTextToSpeechAudioCategory.playAndRecord] category. + // On devices with more than one built-in microphone, + // the audio session uses the microphone closest to the video camera. + // + // Use this mode to ensure that the system provides appropriate audio-signal processing. + kVideoRecording = 6, + // A mode that indicates that your app is performing two-way voice communication, + // such as using Voice over Internet Protocol (VoIP). + // + // Use this mode for Voice over IP (VoIP) apps that use the + // [IosTextToSpeechAudioCategory.playAndRecord] category. + // When you set this mode, the session optimizes the device’s tonal + // equalization for voice and reduces the set of allowable audio routes + // to only those appropriate for voice chat. + // + // Using this mode has the side effect of enabling the + // [IosTextToSpeechAudioCategoryOptions.allowBluetooth] category option. + kVoiceChat = 7, + // A mode that indicates that your app plays audio using text-to-speech. + // + // Setting this mode allows for different routing behaviors when your app + // is connected to certain audio devices, such as CarPlay. + // An example of an app that uses this mode is a turn-by-turn navigation app + // that plays short prompts to the user. + // + // Typically, apps of the same type also configure their sessions to use the + // [IosTextToSpeechAudioCategoryOptions.duckOthers] and + // [IosTextToSpeechAudioCategoryOptions.interruptSpokenAudioAndMixWithOthers] options. + kVoicePrompt = 8 +}; + +// Audio session category options for iOS. +// +// See also: +// * https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions +enum class IosTextToSpeechAudioCategoryOptions { + // An option that indicates whether audio from this session mixes with audio + // from active sessions in other audio apps. + // + // You can set this option explicitly only if the audio session category + // is [IosTextToSpeechAudioCategory.playAndRecord] or + // [IosTextToSpeechAudioCategory.playback]. + // If you set the audio session category to [IosTextToSpeechAudioCategory.ambient], + // the session automatically sets this option. + // Likewise, setting the [duckOthers] or [interruptSpokenAudioAndMixWithOthers] + // options also enables this option. + // + // If you set this option, your app mixes its audio with audio playing + // in background apps, such as the Music app. + kMixWithOthers = 0, + // An option that reduces the volume of other audio sessions while audio + // from this session plays. + // + // You can set this option only if the audio session category is + // [IosTextToSpeechAudioCategory.playAndRecord] or + // [IosTextToSpeechAudioCategory.playback]. + // Setting it implicitly sets the [mixWithOthers] option. + // + // Use this option to mix your app’s audio with that of others. + // While your app plays its audio, the system reduces the volume of other + // audio sessions to make yours more prominent. If your app provides + // occasional spoken audio, such as in a turn-by-turn navigation app + // or an exercise app, you should also set the [interruptSpokenAudioAndMixWithOthers] option. + // + // Note that ducking begins when you activate your app’s audio session + // and ends when you deactivate the session. + // + // See also: + // * [FlutterTts.setSharedInstance] + kDuckOthers = 1, + // An option that determines whether to pause spoken audio content + // from other sessions when your app plays its audio. + // + // You can set this option only if the audio session category is + // [IosTextToSpeechAudioCategory.playAndRecord] or + // [IosTextToSpeechAudioCategory.playback]. + // Setting this option also sets [mixWithOthers]. + // + // If you set this option, the system mixes your audio with other + // audio sessions, but interrupts (and stops) audio sessions that use the + // [IosTextToSpeechAudioMode.spokenAudio] audio session mode. + // It pauses the audio from other apps as long as your session is active. + // After your audio session deactivates, the system resumes the interrupted app’s audio. + // + // Set this option if your app’s audio is occasional and spoken, + // such as in a turn-by-turn navigation app or an exercise app. + // This avoids intelligibility problems when two spoken audio apps mix. + // If you set this option, also set the [duckOthers] option unless + // you have a specific reason not to. Ducking other audio, rather than + // interrupting it, is appropriate when the other audio isn’t spoken audio. + kInterruptSpokenAudioAndMixWithOthers = 2, + // An option that determines whether Bluetooth hands-free devices appear + // as available input routes. + // + // You can set this option only if the audio session category is + // [IosTextToSpeechAudioCategory.playAndRecord] or + // [IosTextToSpeechAudioCategory.playback]. + // + // You’re required to set this option to allow routing audio input and output + // to a paired Bluetooth Hands-Free Profile (HFP) device. + // If you clear this option, paired Bluetooth HFP devices don’t show up + // as available audio input routes. + kAllowBluetooth = 3, + // An option that determines whether you can stream audio from this session + // to Bluetooth devices that support the Advanced Audio Distribution Profile (A2DP). + // + // A2DP is a stereo, output-only profile intended for higher bandwidth + // audio use cases, such as music playback. + // The system automatically routes to A2DP ports if you configure an + // app’s audio session to use the [IosTextToSpeechAudioCategory.ambient], + // [IosTextToSpeechAudioCategory.ambientSolo], or + // [IosTextToSpeechAudioCategory.playback] categories. + // + // Starting with iOS 10.0, apps using the + // [IosTextToSpeechAudioCategory.playAndRecord] category may also allow + // routing output to paired Bluetooth A2DP devices. To enable this behavior, + // pass this category option when setting your audio session’s category. + // + // Note: If this option and the [allowBluetooth] option are both set, + // when a single device supports both the Hands-Free Profile (HFP) and A2DP, + // the system gives hands-free ports a higher priority for routing. + kAllowBluetoothA2DP = 4, + // An option that determines whether you can stream audio + // from this session to AirPlay devices. + // + // Setting this option enables the audio session to route audio output + // to AirPlay devices. You can only explicitly set this option if the + // audio session’s category is set to [IosTextToSpeechAudioCategory.playAndRecord]. + // For most other audio session categories, the system sets this option implicitly. + kAllowAirPlay = 5, + // An option that determines whether audio from the session defaults to the built-in speaker instead of the receiver. + // + // You can set this option only when using the + // [IosTextToSpeechAudioCategory.playAndRecord] category. + // It’s used to modify the category’s routing behavior so that audio + // is always routed to the speaker rather than the receiver if + // no other accessories, such as headphones, are in use. + // + // When using this option, the system honors user gestures. + // For example, plugging in a headset causes the route to change to + // headset mic/headphones, and unplugging the headset causes the route + // to change to built-in mic/speaker (as opposed to built-in mic/receiver) + // when you’ve set this override. + // + // In the case of using a USB input-only accessory, audio input + // comes from the accessory, and the system routes audio to the headphones, + // if attached, or to the speaker if the headphones aren’t plugged in. + // The use case is to route audio to the speaker instead of the receiver + // in cases where the audio would normally go to the receiver. + kDefaultToSpeaker = 6 +}; + +enum class TtsPlatform { + kAndroid = 0, + kIos = 1 +}; + + +// Generated class from Pigeon that represents data sent in messages. +class Voice { + public: + // Constructs an object setting all non-nullable fields. + explicit Voice( + const std::string& name, + const std::string& locale); + + // Constructs an object setting all fields. + explicit Voice( + const std::string& name, + const std::string& locale, + const std::string* gender, + const std::string* quality, + const std::string* identifier); + + const std::string& name() const; + void set_name(std::string_view value_arg); + + const std::string& locale() const; + void set_locale(std::string_view value_arg); + + const std::string* gender() const; + void set_gender(const std::string_view* value_arg); + void set_gender(std::string_view value_arg); + + const std::string* quality() const; + void set_quality(const std::string_view* value_arg); + void set_quality(std::string_view value_arg); + + const std::string* identifier() const; + void set_identifier(const std::string_view* value_arg); + void set_identifier(std::string_view value_arg); + + private: + static Voice FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class TtsHostApi; + friend class IosTtsHostApi; + friend class AndroidTtsHostApi; + friend class MacosTtsHostApi; + friend class TtsFlutterApi; + friend class PigeonInternalCodecSerializer; + std::string name_; + std::string locale_; + std::optional gender_; + std::optional quality_; + std::optional identifier_; +}; + + +// Generated class from Pigeon that represents data sent in messages. +class TtsResult { + public: + // Constructs an object setting all non-nullable fields. + explicit TtsResult(bool success); + + // Constructs an object setting all fields. + explicit TtsResult( + bool success, + const std::string* message); + + bool success() const; + void set_success(bool value_arg); + + const std::string* message() const; + void set_message(const std::string_view* value_arg); + void set_message(std::string_view value_arg); + + private: + static TtsResult FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class TtsHostApi; + friend class IosTtsHostApi; + friend class AndroidTtsHostApi; + friend class MacosTtsHostApi; + friend class TtsFlutterApi; + friend class PigeonInternalCodecSerializer; + bool success_; + std::optional message_; +}; + + +// Generated class from Pigeon that represents data sent in messages. +class TtsProgress { + public: + // Constructs an object setting all fields. + explicit TtsProgress( + const std::string& text, + int64_t start, + int64_t end, + const std::string& word); + + const std::string& text() const; + void set_text(std::string_view value_arg); + + int64_t start() const; + void set_start(int64_t value_arg); + + int64_t end() const; + void set_end(int64_t value_arg); + + const std::string& word() const; + void set_word(std::string_view value_arg); + + private: + static TtsProgress FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class TtsHostApi; + friend class IosTtsHostApi; + friend class AndroidTtsHostApi; + friend class MacosTtsHostApi; + friend class TtsFlutterApi; + friend class PigeonInternalCodecSerializer; + std::string text_; + int64_t start_; + int64_t end_; + std::string word_; +}; + + +// Generated class from Pigeon that represents data sent in messages. +class TtsRateValidRange { + public: + // Constructs an object setting all fields. + explicit TtsRateValidRange( + double minimum, + double normal, + double maximum, + const TtsPlatform& platform); + + double minimum() const; + void set_minimum(double value_arg); + + double normal() const; + void set_normal(double value_arg); + + double maximum() const; + void set_maximum(double value_arg); + + const TtsPlatform& platform() const; + void set_platform(const TtsPlatform& value_arg); + + private: + static TtsRateValidRange FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class TtsHostApi; + friend class IosTtsHostApi; + friend class AndroidTtsHostApi; + friend class MacosTtsHostApi; + friend class TtsFlutterApi; + friend class PigeonInternalCodecSerializer; + double minimum_; + double normal_; + double maximum_; + TtsPlatform platform_; +}; + + +class PigeonInternalCodecSerializer : public flutter::StandardCodecSerializer { + public: + PigeonInternalCodecSerializer(); + inline static PigeonInternalCodecSerializer& GetInstance() { + static PigeonInternalCodecSerializer sInstance; + return sInstance; + } + + void WriteValue( + const flutter::EncodableValue& value, + flutter::ByteStreamWriter* stream) const override; + protected: + flutter::EncodableValue ReadValueOfType( + uint8_t type, + flutter::ByteStreamReader* stream) const override; +}; + +// Generated interface from Pigeon that represents a handler of messages from Flutter. +class TtsHostApi { + public: + TtsHostApi(const TtsHostApi&) = delete; + TtsHostApi& operator=(const TtsHostApi&) = delete; + virtual ~TtsHostApi() {} + virtual void Speak( + const std::string& text, + bool force_focus, + std::function reply)> result) = 0; + virtual void Pause(std::function reply)> result) = 0; + virtual void Stop(std::function reply)> result) = 0; + virtual void SetSpeechRate( + double rate, + std::function reply)> result) = 0; + virtual void SetVolume( + double volume, + std::function reply)> result) = 0; + virtual void SetPitch( + double pitch, + std::function reply)> result) = 0; + virtual void SetVoice( + const Voice& voice, + std::function reply)> result) = 0; + virtual void ClearVoice(std::function reply)> result) = 0; + virtual void AwaitSpeakCompletion( + bool await_completion, + std::function reply)> result) = 0; + virtual void GetLanguages(std::function reply)> result) = 0; + virtual void GetVoices(std::function reply)> result) = 0; + + // The codec used by TtsHostApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `TtsHostApi` to handle messages through the `binary_messenger`. + static void SetUp( + flutter::BinaryMessenger* binary_messenger, + TtsHostApi* api); + static void SetUp( + flutter::BinaryMessenger* binary_messenger, + TtsHostApi* api, + const std::string& message_channel_suffix); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + protected: + TtsHostApi() = default; +}; +// Generated interface from Pigeon that represents a handler of messages from Flutter. +class IosTtsHostApi { + public: + IosTtsHostApi(const IosTtsHostApi&) = delete; + IosTtsHostApi& operator=(const IosTtsHostApi&) = delete; + virtual ~IosTtsHostApi() {} + virtual void AwaitSynthCompletion( + bool await_completion, + std::function reply)> result) = 0; + virtual void SynthesizeToFile( + const std::string& text, + const std::string& file_name, + bool is_full_path, + std::function reply)> result) = 0; + virtual void SetSharedInstance( + bool shared_session, + std::function reply)> result) = 0; + virtual void AutoStopSharedSession( + bool auto_stop, + std::function reply)> result) = 0; + virtual void SetIosAudioCategory( + const IosTextToSpeechAudioCategory& category, + const flutter::EncodableList& options, + const IosTextToSpeechAudioMode& mode, + std::function reply)> result) = 0; + virtual void GetSpeechRateValidRange(std::function reply)> result) = 0; + virtual void IsLanguageAvailable( + const std::string& language, + std::function reply)> result) = 0; + virtual void SetLanguange( + const std::string& language, + std::function reply)> result) = 0; + + // The codec used by IosTtsHostApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `IosTtsHostApi` to handle messages through the `binary_messenger`. + static void SetUp( + flutter::BinaryMessenger* binary_messenger, + IosTtsHostApi* api); + static void SetUp( + flutter::BinaryMessenger* binary_messenger, + IosTtsHostApi* api, + const std::string& message_channel_suffix); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + protected: + IosTtsHostApi() = default; +}; +// Generated interface from Pigeon that represents a handler of messages from Flutter. +class AndroidTtsHostApi { + public: + AndroidTtsHostApi(const AndroidTtsHostApi&) = delete; + AndroidTtsHostApi& operator=(const AndroidTtsHostApi&) = delete; + virtual ~AndroidTtsHostApi() {} + virtual void AwaitSynthCompletion( + bool await_completion, + std::function reply)> result) = 0; + virtual void GetMaxSpeechInputLength(std::function> reply)> result) = 0; + virtual void SetEngine( + const std::string& engine, + std::function reply)> result) = 0; + virtual void GetEngines(std::function reply)> result) = 0; + virtual void GetDefaultEngine(std::function> reply)> result) = 0; + virtual void GetDefaultVoice(std::function> reply)> result) = 0; + // [Future] which invokes the platform specific method for synthesizeToFile + virtual void SynthesizeToFile( + const std::string& text, + const std::string& file_name, + bool is_full_path, + std::function reply)> result) = 0; + virtual void IsLanguageInstalled( + const std::string& language, + std::function reply)> result) = 0; + virtual void IsLanguageAvailable( + const std::string& language, + std::function reply)> result) = 0; + virtual void AreLanguagesInstalled( + const flutter::EncodableList& languages, + std::function reply)> result) = 0; + virtual void GetSpeechRateValidRange(std::function reply)> result) = 0; + virtual void SetSilence( + int64_t timems, + std::function reply)> result) = 0; + virtual void SetQueueMode( + int64_t queue_mode, + std::function reply)> result) = 0; + virtual void SetAudioAttributesForNavigation(std::function reply)> result) = 0; + + // The codec used by AndroidTtsHostApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `AndroidTtsHostApi` to handle messages through the `binary_messenger`. + static void SetUp( + flutter::BinaryMessenger* binary_messenger, + AndroidTtsHostApi* api); + static void SetUp( + flutter::BinaryMessenger* binary_messenger, + AndroidTtsHostApi* api, + const std::string& message_channel_suffix); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + protected: + AndroidTtsHostApi() = default; +}; +// Generated interface from Pigeon that represents a handler of messages from Flutter. +class MacosTtsHostApi { + public: + MacosTtsHostApi(const MacosTtsHostApi&) = delete; + MacosTtsHostApi& operator=(const MacosTtsHostApi&) = delete; + virtual ~MacosTtsHostApi() {} + virtual void AwaitSynthCompletion( + bool await_completion, + std::function reply)> result) = 0; + virtual void GetSpeechRateValidRange(std::function reply)> result) = 0; + virtual void SetLanguange( + const std::string& language, + std::function reply)> result) = 0; + virtual void IsLanguageAvailable( + const std::string& language, + std::function reply)> result) = 0; + + // The codec used by MacosTtsHostApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `MacosTtsHostApi` to handle messages through the `binary_messenger`. + static void SetUp( + flutter::BinaryMessenger* binary_messenger, + MacosTtsHostApi* api); + static void SetUp( + flutter::BinaryMessenger* binary_messenger, + MacosTtsHostApi* api, + const std::string& message_channel_suffix); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + protected: + MacosTtsHostApi() = default; +}; +// Generated class from Pigeon that represents Flutter messages that can be called from C++. +class TtsFlutterApi { + public: + TtsFlutterApi(flutter::BinaryMessenger* binary_messenger); + TtsFlutterApi( + flutter::BinaryMessenger* binary_messenger, + const std::string& message_channel_suffix); + static const flutter::StandardMessageCodec& GetCodec(); + void OnSpeakStartCb( + std::function&& on_success, + std::function&& on_error); + void OnSpeakCompleteCb( + std::function&& on_success, + std::function&& on_error); + void OnSpeakPauseCb( + std::function&& on_success, + std::function&& on_error); + void OnSpeakResumeCb( + std::function&& on_success, + std::function&& on_error); + void OnSpeakCancelCb( + std::function&& on_success, + std::function&& on_error); + void OnSpeakProgressCb( + const TtsProgress& progress, + std::function&& on_success, + std::function&& on_error); + void OnSpeakErrorCb( + const std::string& error, + std::function&& on_success, + std::function&& on_error); + void OnSynthStartCb( + std::function&& on_success, + std::function&& on_error); + void OnSynthCompleteCb( + std::function&& on_success, + std::function&& on_error); + void OnSynthErrorCb( + const std::string& error, + std::function&& on_success, + std::function&& on_error); + private: + flutter::BinaryMessenger* binary_messenger_; + std::string message_channel_suffix_; +}; + +} // namespace flutter_tts +#endif // PIGEON_MESSAGES_G_H_ From 366abdcf2a2492dde130f30445b553f79cdc0c51 Mon Sep 17 00:00:00 2001 From: von Date: Sat, 15 Nov 2025 17:43:25 +0800 Subject: [PATCH 2/5] build: use federated Plugin structure --- .vscode/launch.json | 52 ++--- {example => apps/example}/.gitignore | 0 {example => apps/example}/.metadata | 0 {example => apps/example}/README.md | 0 .../example}/analysis_options.yaml | 0 {example => apps/example}/android/.gitignore | 0 .../example}/android/app/.classpath | 0 .../org.eclipse.buildship.core.prefs | 0 .../example}/android/app/build.gradle.kts | 0 .../android/app/src/debug/AndroidManifest.xml | 0 .../android/app/src/main/AndroidManifest.xml | 0 .../main/res/drawable/launch_background.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../app/src/main/res/values/styles.xml | 0 .../app/src/profile/AndroidManifest.xml | 0 .../example}/android/build.gradle.kts | 0 .../example}/android/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 .../example}/android/settings.gradle.kts | 0 {example => apps/example}/ios/.gitignore | 0 .../ios/Flutter/AppFrameworkInfo.plist | 0 .../example}/ios/Flutter/Debug.xcconfig | 0 .../example}/ios/Flutter/Release.xcconfig | 0 {example => apps/example}/ios/Podfile | 0 .../ios/Runner.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../example}/ios/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../Icon-App-1024x1024@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin .../Icon-App-83.5x83.5@2x.png | Bin .../LaunchImage.imageset/Contents.json | 0 .../LaunchImage.imageset/LaunchImage.png | Bin .../LaunchImage.imageset/LaunchImage@2x.png | Bin .../LaunchImage.imageset/LaunchImage@3x.png | Bin .../LaunchImage.imageset/README.md | 0 .../Runner/Base.lproj/LaunchScreen.storyboard | 0 .../ios/Runner/Base.lproj/Main.storyboard | 0 .../example}/ios/Runner/Info.plist | 0 .../ios/Runner/Runner-Bridging-Header.h | 0 .../ios/RunnerTests/RunnerTests.swift | 0 {example => apps/example}/lib/main.dart | 3 +- {example => apps/example}/macos/.gitignore | 0 .../macos/Flutter/Flutter-Debug.xcconfig | 0 .../macos/Flutter/Flutter-Release.xcconfig | 0 .../Flutter/GeneratedPluginRegistrant.swift | 2 +- {example => apps/example}/macos/Podfile | 0 .../macos/Runner.xcodeproj/project.pbxproj | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../example}/macos/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/app_icon_1024.png | Bin .../AppIcon.appiconset/app_icon_128.png | Bin .../AppIcon.appiconset/app_icon_16.png | Bin .../AppIcon.appiconset/app_icon_256.png | Bin .../AppIcon.appiconset/app_icon_32.png | Bin .../AppIcon.appiconset/app_icon_512.png | Bin .../AppIcon.appiconset/app_icon_64.png | Bin .../macos/Runner/Base.lproj/MainMenu.xib | 0 .../macos/Runner/Configs/AppInfo.xcconfig | 0 .../macos/Runner/Configs/Debug.xcconfig | 0 .../macos/Runner/Configs/Release.xcconfig | 0 .../macos/Runner/Configs/Warnings.xcconfig | 0 .../macos/Runner/DebugProfile.entitlements | 0 .../example}/macos/Runner/Info.plist | 0 .../macos/Runner/MainFlutterWindow.swift | 0 .../macos/Runner/Release.entitlements | 0 .../macos/RunnerTests/RunnerTests.swift | 0 {example => apps/example}/pubspec.yaml | 10 +- .../example}/test/widget_test.dart | 0 {example => apps/example}/web/favicon.png | Bin .../example}/web/icons/Icon-192.png | Bin .../example}/web/icons/Icon-512.png | Bin {example => apps/example}/web/index.html | 0 {example => apps/example}/web/manifest.json | 0 {example => apps/example}/windows/.gitignore | 0 .../example}/windows/CMakeLists.txt | 0 .../example}/windows/flutter/CMakeLists.txt | 0 .../example}/windows/runner/CMakeLists.txt | 0 .../example}/windows/runner/Runner.rc | 0 .../windows/runner/flutter_window.cpp | 0 .../example}/windows/runner/flutter_window.h | 0 .../example}/windows/runner/main.cpp | 0 .../example}/windows/runner/resource.h | 0 .../windows/runner/resources/app_icon.ico | Bin .../windows/runner/runner.exe.manifest | 0 .../example}/windows/runner/utils.cpp | 0 .../example}/windows/runner/utils.h | 0 .../example}/windows/runner/win32_window.cpp | 0 .../example}/windows/runner/win32_window.h | 0 {example => apps/example}/winuwp/.gitignore | 0 .../example}/winuwp/CMakeLists.txt | 0 .../example}/winuwp/flutter/CMakeLists.txt | 0 .../example}/winuwp/flutter/flutter_windows.h | 0 .../example}/winuwp/project_version | 0 .../runner_uwp/Assets/LargeTile.scale-100.png | Bin .../runner_uwp/Assets/LargeTile.scale-125.png | Bin .../runner_uwp/Assets/LargeTile.scale-150.png | Bin .../runner_uwp/Assets/LargeTile.scale-200.png | Bin .../runner_uwp/Assets/LargeTile.scale-400.png | Bin .../Assets/LockScreenLogo.scale-200.png | Bin .../runner_uwp/Assets/SmallTile.scale-100.png | Bin .../runner_uwp/Assets/SmallTile.scale-125.png | Bin .../runner_uwp/Assets/SmallTile.scale-150.png | Bin .../runner_uwp/Assets/SmallTile.scale-200.png | Bin .../runner_uwp/Assets/SmallTile.scale-400.png | Bin .../Assets/SplashScreen.scale-100.png | Bin .../Assets/SplashScreen.scale-125.png | Bin .../Assets/SplashScreen.scale-150.png | Bin .../Assets/SplashScreen.scale-200.png | Bin .../Assets/SplashScreen.scale-400.png | Bin .../Assets/Square150x150Logo.scale-100.png | Bin .../Assets/Square150x150Logo.scale-125.png | Bin .../Assets/Square150x150Logo.scale-150.png | Bin .../Assets/Square150x150Logo.scale-200.png | Bin .../Assets/Square150x150Logo.scale-400.png | Bin ...x44Logo.altform-unplated_targetsize-16.png | Bin ...44Logo.altform-unplated_targetsize-256.png | Bin ...x44Logo.altform-unplated_targetsize-32.png | Bin ...x44Logo.altform-unplated_targetsize-48.png | Bin .../Assets/Square44x44Logo.scale-100.png | Bin .../Assets/Square44x44Logo.scale-125.png | Bin .../Assets/Square44x44Logo.scale-150.png | Bin .../Assets/Square44x44Logo.scale-200.png | Bin .../Assets/Square44x44Logo.scale-400.png | Bin .../Assets/Square44x44Logo.targetsize-16.png | Bin .../Assets/Square44x44Logo.targetsize-24.png | Bin ...x44Logo.targetsize-24_altform-unplated.png | Bin .../Assets/Square44x44Logo.targetsize-256.png | Bin .../Assets/Square44x44Logo.targetsize-32.png | Bin .../Assets/Square44x44Logo.targetsize-48.png | Bin .../winuwp/runner_uwp/Assets/StoreLogo.png | Bin .../runner_uwp/Assets/StoreLogo.scale-100.png | Bin .../runner_uwp/Assets/StoreLogo.scale-125.png | Bin .../runner_uwp/Assets/StoreLogo.scale-150.png | Bin .../runner_uwp/Assets/StoreLogo.scale-200.png | Bin .../runner_uwp/Assets/StoreLogo.scale-400.png | Bin .../Assets/Wide310x150Logo.scale-200.png | Bin .../runner_uwp/Assets/WideTile.scale-100.png | Bin .../runner_uwp/Assets/WideTile.scale-125.png | Bin .../runner_uwp/Assets/WideTile.scale-150.png | Bin .../runner_uwp/Assets/WideTile.scale-200.png | Bin .../runner_uwp/Assets/WideTile.scale-400.png | Bin .../example}/winuwp/runner_uwp/CMakeLists.txt | 0 .../winuwp/runner_uwp/CMakeSettings.json | 0 .../runner_uwp/Windows_TemporaryKey.pfx | Bin .../winuwp/runner_uwp/appxmanifest.in | 0 .../runner_uwp/flutter_frameworkview.cpp | 0 .../example}/winuwp/runner_uwp/main.cpp | 0 .../example}/winuwp/runner_uwp/resources.pri | Bin lib/flutter_tts.dart | 5 - lib/src/flutter_tts.dart | 5 - lib/src/flutter_tts_ios.dart | 107 ---------- lib/src/flutter_tts_macos.dart | 53 ----- lib/src/flutter_tts_native.dart | 4 - lib/src/flutter_tts_platform_interface.dart | 81 -------- packages/.gitignore | 48 +++++ packages/flutter_tts/README.md | 26 +++ packages/flutter_tts/analysis_options.yaml | 4 + packages/flutter_tts/coverage_badge.svg | 20 ++ packages/flutter_tts/lib/flutter_tts.dart | 16 ++ packages/flutter_tts/pubspec.yaml | 40 ++++ packages/flutter_tts_android/README.md | 14 ++ .../flutter_tts_android/analysis_options.yaml | 4 + .../flutter_tts_android/android}/.gitignore | 0 .../flutter_tts_android/android}/build.gradle | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 .../android}/settings.gradle | 0 .../android}/src/main/AndroidManifest.xml | 0 .../tundralabs/fluttertts/FlutterTtsPlugin.kt | 54 +++-- .../com/tundralabs/fluttertts/messages.g.kt | 0 .../lib}/flutter_tts_android.dart | 83 ++++---- packages/flutter_tts_android/pubspec.yaml | 23 +++ packages/flutter_tts_ios/.gitignore | 2 + packages/flutter_tts_ios/README.md | 14 ++ .../flutter_tts_ios/analysis_options.yaml | 4 + .../flutter_tts_ios/ios}/.gitignore | 0 .../flutter_tts_ios/ios}/Assets/.gitkeep | 0 .../ios}/Classes/AudioCategory.swift | 0 .../ios}/Classes/AudioCategoryOptions.swift | 0 .../ios}/Classes/AudioModes.swift | 0 .../ios}/Classes/SwiftFlutterTtsPlugin.swift | 0 .../ios}/Classes/message.g.swift | 0 .../ios/flutter_tts_ios.podspec | 2 +- .../flutter_tts_ios/lib/flutter_tts_ios.dart | 119 +++++++++++ packages/flutter_tts_ios/pubspec.yaml | 28 +++ packages/flutter_tts_macos/.gitignore | 3 + packages/flutter_tts_macos/README.md | 14 ++ .../flutter_tts_macos/analysis_options.yaml | 4 + .../lib/flutter_tts_macos.dart | 59 ++++++ .../macos}/Classes/FlutterTtsPlugin.swift | 0 .../macos}/Classes/message.g.swift | 0 .../macos/flutter_tts_macos.podspec | 2 +- packages/flutter_tts_macos/pubspec.yaml | 28 +++ .../flutter_tts_platform_interface/README.md | 14 ++ .../analysis_options.yaml | 7 + .../lib/flutter_tts_platform_interface.dart | 146 ++++++++++++++ .../lib/src/flutter_tts_method_channel.dart | 5 + .../lib/src/flutter_tts_mixin.dart | 33 +++- .../lib}/src/messages.g.dart | 0 .../pubspec.yaml | 20 ++ packages/flutter_tts_web/.gitignore | 3 + packages/flutter_tts_web/README.md | 14 ++ .../flutter_tts_web/analysis_options.yaml | 6 + .../flutter_tts_web/lib}/flutter_tts_web.dart | 186 ++++++++++-------- .../lib}/flutter_tts_web_interop_types.dart | 12 ++ packages/flutter_tts_web/pubspec.yaml | 30 +++ packages/flutter_tts_windows/.gitignore | 29 +++ packages/flutter_tts_windows/.metadata | 10 + packages/flutter_tts_windows/README.md | 14 ++ .../flutter_tts_windows/analysis_options.yaml | 1 + .../lib/flutter_tts_windows.dart | 9 + packages/flutter_tts_windows/pubspec.yaml | 27 +++ .../flutter_tts_windows/windows}/.gitignore | 0 .../windows}/CMakeLists.txt | 4 +- .../windows}/flutter_tts_plugin.cpp | 4 +- .../flutter_tts_windows/flutter_tts_windows.h | 2 +- .../windows}/messages.g.cpp | 2 +- .../flutter_tts_windows/windows}/messages.g.h | 2 +- pigeons/messages.dart | 10 +- pubspec.yaml | 58 ++---- tools/generate_pigeons.dart | 24 +-- 247 files changed, 1107 insertions(+), 498 deletions(-) rename {example => apps/example}/.gitignore (100%) rename {example => apps/example}/.metadata (100%) rename {example => apps/example}/README.md (100%) rename {example => apps/example}/analysis_options.yaml (100%) rename {example => apps/example}/android/.gitignore (100%) rename {example => apps/example}/android/app/.classpath (100%) rename {example => apps/example}/android/app/.settings/org.eclipse.buildship.core.prefs (100%) rename {example => apps/example}/android/app/build.gradle.kts (100%) rename {example => apps/example}/android/app/src/debug/AndroidManifest.xml (100%) rename {example => apps/example}/android/app/src/main/AndroidManifest.xml (100%) rename {example => apps/example}/android/app/src/main/res/drawable/launch_background.xml (100%) rename {example => apps/example}/android/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename {example => apps/example}/android/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename {example => apps/example}/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {example => apps/example}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {example => apps/example}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename {example => apps/example}/android/app/src/main/res/values/styles.xml (100%) rename {example => apps/example}/android/app/src/profile/AndroidManifest.xml (100%) rename {example => apps/example}/android/build.gradle.kts (100%) rename {example => apps/example}/android/gradle.properties (100%) rename {example => apps/example}/android/gradle/wrapper/gradle-wrapper.properties (100%) rename {example => apps/example}/android/settings.gradle.kts (100%) rename {example => apps/example}/ios/.gitignore (100%) rename {example => apps/example}/ios/Flutter/AppFrameworkInfo.plist (100%) rename {example => apps/example}/ios/Flutter/Debug.xcconfig (100%) rename {example => apps/example}/ios/Flutter/Release.xcconfig (100%) rename {example => apps/example}/ios/Podfile (100%) rename {example => apps/example}/ios/Runner.xcodeproj/project.pbxproj (100%) rename {example => apps/example}/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename {example => apps/example}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example => apps/example}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {example => apps/example}/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename {example => apps/example}/ios/Runner.xcworkspace/contents.xcworkspacedata (100%) rename {example => apps/example}/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example => apps/example}/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {example => apps/example}/ios/Runner/AppDelegate.swift (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png (100%) rename {example => apps/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md (100%) rename {example => apps/example}/ios/Runner/Base.lproj/LaunchScreen.storyboard (100%) rename {example => apps/example}/ios/Runner/Base.lproj/Main.storyboard (100%) rename {example => apps/example}/ios/Runner/Info.plist (100%) rename {example => apps/example}/ios/Runner/Runner-Bridging-Header.h (100%) rename {example => apps/example}/ios/RunnerTests/RunnerTests.swift (100%) rename {example => apps/example}/lib/main.dart (99%) rename {example => apps/example}/macos/.gitignore (100%) rename {example => apps/example}/macos/Flutter/Flutter-Debug.xcconfig (100%) rename {example => apps/example}/macos/Flutter/Flutter-Release.xcconfig (100%) rename {example => apps/example}/macos/Flutter/GeneratedPluginRegistrant.swift (90%) rename {example => apps/example}/macos/Podfile (100%) rename {example => apps/example}/macos/Runner.xcodeproj/project.pbxproj (100%) rename {example => apps/example}/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example => apps/example}/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename {example => apps/example}/macos/Runner.xcworkspace/contents.xcworkspacedata (100%) rename {example => apps/example}/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example => apps/example}/macos/Runner/AppDelegate.swift (100%) rename {example => apps/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {example => apps/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png (100%) rename {example => apps/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png (100%) rename {example => apps/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png (100%) rename {example => apps/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png (100%) rename {example => apps/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png (100%) rename {example => apps/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png (100%) rename {example => apps/example}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png (100%) rename {example => apps/example}/macos/Runner/Base.lproj/MainMenu.xib (100%) rename {example => apps/example}/macos/Runner/Configs/AppInfo.xcconfig (100%) rename {example => apps/example}/macos/Runner/Configs/Debug.xcconfig (100%) rename {example => apps/example}/macos/Runner/Configs/Release.xcconfig (100%) rename {example => apps/example}/macos/Runner/Configs/Warnings.xcconfig (100%) rename {example => apps/example}/macos/Runner/DebugProfile.entitlements (100%) rename {example => apps/example}/macos/Runner/Info.plist (100%) rename {example => apps/example}/macos/Runner/MainFlutterWindow.swift (100%) rename {example => apps/example}/macos/Runner/Release.entitlements (100%) rename {example => apps/example}/macos/RunnerTests/RunnerTests.swift (100%) rename {example => apps/example}/pubspec.yaml (94%) rename {example => apps/example}/test/widget_test.dart (100%) rename {example => apps/example}/web/favicon.png (100%) rename {example => apps/example}/web/icons/Icon-192.png (100%) rename {example => apps/example}/web/icons/Icon-512.png (100%) rename {example => apps/example}/web/index.html (100%) rename {example => apps/example}/web/manifest.json (100%) rename {example => apps/example}/windows/.gitignore (100%) rename {example => apps/example}/windows/CMakeLists.txt (100%) rename {example => apps/example}/windows/flutter/CMakeLists.txt (100%) rename {example => apps/example}/windows/runner/CMakeLists.txt (100%) rename {example => apps/example}/windows/runner/Runner.rc (100%) rename {example => apps/example}/windows/runner/flutter_window.cpp (100%) rename {example => apps/example}/windows/runner/flutter_window.h (100%) rename {example => apps/example}/windows/runner/main.cpp (100%) rename {example => apps/example}/windows/runner/resource.h (100%) rename {example => apps/example}/windows/runner/resources/app_icon.ico (100%) rename {example => apps/example}/windows/runner/runner.exe.manifest (100%) rename {example => apps/example}/windows/runner/utils.cpp (100%) rename {example => apps/example}/windows/runner/utils.h (100%) rename {example => apps/example}/windows/runner/win32_window.cpp (100%) rename {example => apps/example}/windows/runner/win32_window.h (100%) rename {example => apps/example}/winuwp/.gitignore (100%) rename {example => apps/example}/winuwp/CMakeLists.txt (100%) rename {example => apps/example}/winuwp/flutter/CMakeLists.txt (100%) rename {example => apps/example}/winuwp/flutter/flutter_windows.h (100%) rename {example => apps/example}/winuwp/project_version (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/LargeTile.scale-100.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/LargeTile.scale-125.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/LargeTile.scale-150.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/LargeTile.scale-200.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/LargeTile.scale-400.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/LockScreenLogo.scale-200.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/SmallTile.scale-100.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/SmallTile.scale-125.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/SmallTile.scale-150.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/SmallTile.scale-200.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/SmallTile.scale-400.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/SplashScreen.scale-100.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/SplashScreen.scale-125.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/SplashScreen.scale-150.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/SplashScreen.scale-200.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/SplashScreen.scale-400.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square150x150Logo.scale-100.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square150x150Logo.scale-125.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square150x150Logo.scale-150.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square150x150Logo.scale-200.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square150x150Logo.scale-400.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-16.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-256.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-32.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-48.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.scale-100.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.scale-125.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.scale-150.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.scale-200.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.scale-400.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-16.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-24.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-24_altform-unplated.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-256.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-32.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-48.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/StoreLogo.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/StoreLogo.scale-100.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/StoreLogo.scale-125.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/StoreLogo.scale-150.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/StoreLogo.scale-200.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/StoreLogo.scale-400.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/Wide310x150Logo.scale-200.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/WideTile.scale-100.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/WideTile.scale-125.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/WideTile.scale-150.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/WideTile.scale-200.png (100%) rename {example => apps/example}/winuwp/runner_uwp/Assets/WideTile.scale-400.png (100%) rename {example => apps/example}/winuwp/runner_uwp/CMakeLists.txt (100%) rename {example => apps/example}/winuwp/runner_uwp/CMakeSettings.json (100%) rename {example => apps/example}/winuwp/runner_uwp/Windows_TemporaryKey.pfx (100%) rename {example => apps/example}/winuwp/runner_uwp/appxmanifest.in (100%) rename {example => apps/example}/winuwp/runner_uwp/flutter_frameworkview.cpp (100%) rename {example => apps/example}/winuwp/runner_uwp/main.cpp (100%) rename {example => apps/example}/winuwp/runner_uwp/resources.pri (100%) delete mode 100644 lib/flutter_tts.dart delete mode 100644 lib/src/flutter_tts.dart delete mode 100644 lib/src/flutter_tts_ios.dart delete mode 100644 lib/src/flutter_tts_macos.dart delete mode 100644 lib/src/flutter_tts_native.dart delete mode 100644 lib/src/flutter_tts_platform_interface.dart create mode 100644 packages/.gitignore create mode 100644 packages/flutter_tts/README.md create mode 100644 packages/flutter_tts/analysis_options.yaml create mode 100644 packages/flutter_tts/coverage_badge.svg create mode 100644 packages/flutter_tts/lib/flutter_tts.dart create mode 100644 packages/flutter_tts/pubspec.yaml create mode 100644 packages/flutter_tts_android/README.md create mode 100644 packages/flutter_tts_android/analysis_options.yaml rename {android => packages/flutter_tts_android/android}/.gitignore (100%) rename {android => packages/flutter_tts_android/android}/build.gradle (100%) rename {android => packages/flutter_tts_android/android}/gradle/wrapper/gradle-wrapper.properties (100%) rename {android => packages/flutter_tts_android/android}/settings.gradle (100%) rename {android => packages/flutter_tts_android/android}/src/main/AndroidManifest.xml (100%) rename {android => packages/flutter_tts_android/android}/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt (96%) rename {android => packages/flutter_tts_android/android}/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt (100%) rename {lib/src => packages/flutter_tts_android/lib}/flutter_tts_android.dart (63%) create mode 100644 packages/flutter_tts_android/pubspec.yaml create mode 100644 packages/flutter_tts_ios/.gitignore create mode 100644 packages/flutter_tts_ios/README.md create mode 100644 packages/flutter_tts_ios/analysis_options.yaml rename {ios => packages/flutter_tts_ios/ios}/.gitignore (100%) rename {ios => packages/flutter_tts_ios/ios}/Assets/.gitkeep (100%) rename {ios => packages/flutter_tts_ios/ios}/Classes/AudioCategory.swift (100%) rename {ios => packages/flutter_tts_ios/ios}/Classes/AudioCategoryOptions.swift (100%) rename {ios => packages/flutter_tts_ios/ios}/Classes/AudioModes.swift (100%) rename {ios => packages/flutter_tts_ios/ios}/Classes/SwiftFlutterTtsPlugin.swift (100%) rename {ios => packages/flutter_tts_ios/ios}/Classes/message.g.swift (100%) rename ios/flutter_tts.podspec => packages/flutter_tts_ios/ios/flutter_tts_ios.podspec (94%) create mode 100644 packages/flutter_tts_ios/lib/flutter_tts_ios.dart create mode 100644 packages/flutter_tts_ios/pubspec.yaml create mode 100644 packages/flutter_tts_macos/.gitignore create mode 100644 packages/flutter_tts_macos/README.md create mode 100644 packages/flutter_tts_macos/analysis_options.yaml create mode 100644 packages/flutter_tts_macos/lib/flutter_tts_macos.dart rename {macos => packages/flutter_tts_macos/macos}/Classes/FlutterTtsPlugin.swift (100%) rename {macos => packages/flutter_tts_macos/macos}/Classes/message.g.swift (100%) rename macos/flutter_tts.podspec => packages/flutter_tts_macos/macos/flutter_tts_macos.podspec (94%) create mode 100644 packages/flutter_tts_macos/pubspec.yaml create mode 100644 packages/flutter_tts_platform_interface/README.md create mode 100644 packages/flutter_tts_platform_interface/analysis_options.yaml create mode 100644 packages/flutter_tts_platform_interface/lib/flutter_tts_platform_interface.dart create mode 100644 packages/flutter_tts_platform_interface/lib/src/flutter_tts_method_channel.dart rename lib/src/flutter_tts_method_channel.dart => packages/flutter_tts_platform_interface/lib/src/flutter_tts_mixin.dart (78%) rename {lib => packages/flutter_tts_platform_interface/lib}/src/messages.g.dart (100%) create mode 100644 packages/flutter_tts_platform_interface/pubspec.yaml create mode 100644 packages/flutter_tts_web/.gitignore create mode 100644 packages/flutter_tts_web/README.md create mode 100644 packages/flutter_tts_web/analysis_options.yaml rename {lib/src => packages/flutter_tts_web/lib}/flutter_tts_web.dart (51%) rename {lib/src => packages/flutter_tts_web/lib}/flutter_tts_web_interop_types.dart (75%) create mode 100644 packages/flutter_tts_web/pubspec.yaml create mode 100644 packages/flutter_tts_windows/.gitignore create mode 100644 packages/flutter_tts_windows/.metadata create mode 100644 packages/flutter_tts_windows/README.md create mode 100644 packages/flutter_tts_windows/analysis_options.yaml create mode 100644 packages/flutter_tts_windows/lib/flutter_tts_windows.dart create mode 100644 packages/flutter_tts_windows/pubspec.yaml rename {windows => packages/flutter_tts_windows/windows}/.gitignore (100%) rename {windows => packages/flutter_tts_windows/windows}/CMakeLists.txt (96%) rename {windows => packages/flutter_tts_windows/windows}/flutter_tts_plugin.cpp (99%) rename windows/include/flutter_tts/flutter_tts_plugin.h => packages/flutter_tts_windows/windows/include/flutter_tts_windows/flutter_tts_windows.h (87%) rename {windows => packages/flutter_tts_windows/windows}/messages.g.cpp (99%) rename {windows => packages/flutter_tts_windows/windows}/messages.g.h (99%) diff --git a/.vscode/launch.json b/.vscode/launch.json index 72fc8aa3..898b39c1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,28 +1,28 @@ { - "configurations": [ - { - "name": "Example (Debug)", - "type": "dart", - "request": "launch", - "cwd": "example", - "program": "lib/main.dart", - "flutterMode": "debug" - }, - { - "name": "Example (Release)", - "type": "dart", - "request": "launch", - "cwd": "example", - "program": "lib/main.dart", - "flutterMode": "release" - }, - { - "name": "Example (Profile)", - "type": "dart", - "request": "launch", - "cwd": "example", - "program": "lib/main.dart", - "flutterMode": "profile" - } - ] + "configurations": [ + { + "name": "Example (Debug)", + "type": "dart", + "request": "launch", + "cwd": "apps/example", + "program": "lib/main.dart", + "flutterMode": "debug" + }, + { + "name": "Example (Release)", + "type": "dart", + "request": "launch", + "cwd": "apps/example", + "program": "lib/main.dart", + "flutterMode": "release" + }, + { + "name": "Example (Profile)", + "type": "dart", + "request": "launch", + "cwd": "apps/example", + "program": "lib/main.dart", + "flutterMode": "profile" + } + ] } diff --git a/example/.gitignore b/apps/example/.gitignore similarity index 100% rename from example/.gitignore rename to apps/example/.gitignore diff --git a/example/.metadata b/apps/example/.metadata similarity index 100% rename from example/.metadata rename to apps/example/.metadata diff --git a/example/README.md b/apps/example/README.md similarity index 100% rename from example/README.md rename to apps/example/README.md diff --git a/example/analysis_options.yaml b/apps/example/analysis_options.yaml similarity index 100% rename from example/analysis_options.yaml rename to apps/example/analysis_options.yaml diff --git a/example/android/.gitignore b/apps/example/android/.gitignore similarity index 100% rename from example/android/.gitignore rename to apps/example/android/.gitignore diff --git a/example/android/app/.classpath b/apps/example/android/app/.classpath similarity index 100% rename from example/android/app/.classpath rename to apps/example/android/app/.classpath diff --git a/example/android/app/.settings/org.eclipse.buildship.core.prefs b/apps/example/android/app/.settings/org.eclipse.buildship.core.prefs similarity index 100% rename from example/android/app/.settings/org.eclipse.buildship.core.prefs rename to apps/example/android/app/.settings/org.eclipse.buildship.core.prefs diff --git a/example/android/app/build.gradle.kts b/apps/example/android/app/build.gradle.kts similarity index 100% rename from example/android/app/build.gradle.kts rename to apps/example/android/app/build.gradle.kts diff --git a/example/android/app/src/debug/AndroidManifest.xml b/apps/example/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from example/android/app/src/debug/AndroidManifest.xml rename to apps/example/android/app/src/debug/AndroidManifest.xml diff --git a/example/android/app/src/main/AndroidManifest.xml b/apps/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from example/android/app/src/main/AndroidManifest.xml rename to apps/example/android/app/src/main/AndroidManifest.xml diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/apps/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from example/android/app/src/main/res/drawable/launch_background.xml rename to apps/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/values/styles.xml b/apps/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from example/android/app/src/main/res/values/styles.xml rename to apps/example/android/app/src/main/res/values/styles.xml diff --git a/example/android/app/src/profile/AndroidManifest.xml b/apps/example/android/app/src/profile/AndroidManifest.xml similarity index 100% rename from example/android/app/src/profile/AndroidManifest.xml rename to apps/example/android/app/src/profile/AndroidManifest.xml diff --git a/example/android/build.gradle.kts b/apps/example/android/build.gradle.kts similarity index 100% rename from example/android/build.gradle.kts rename to apps/example/android/build.gradle.kts diff --git a/example/android/gradle.properties b/apps/example/android/gradle.properties similarity index 100% rename from example/android/gradle.properties rename to apps/example/android/gradle.properties diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/apps/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from example/android/gradle/wrapper/gradle-wrapper.properties rename to apps/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/example/android/settings.gradle.kts b/apps/example/android/settings.gradle.kts similarity index 100% rename from example/android/settings.gradle.kts rename to apps/example/android/settings.gradle.kts diff --git a/example/ios/.gitignore b/apps/example/ios/.gitignore similarity index 100% rename from example/ios/.gitignore rename to apps/example/ios/.gitignore diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/apps/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from example/ios/Flutter/AppFrameworkInfo.plist rename to apps/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/example/ios/Flutter/Debug.xcconfig b/apps/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from example/ios/Flutter/Debug.xcconfig rename to apps/example/ios/Flutter/Debug.xcconfig diff --git a/example/ios/Flutter/Release.xcconfig b/apps/example/ios/Flutter/Release.xcconfig similarity index 100% rename from example/ios/Flutter/Release.xcconfig rename to apps/example/ios/Flutter/Release.xcconfig diff --git a/example/ios/Podfile b/apps/example/ios/Podfile similarity index 100% rename from example/ios/Podfile rename to apps/example/ios/Podfile diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/apps/example/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from example/ios/Runner.xcodeproj/project.pbxproj rename to apps/example/ios/Runner.xcodeproj/project.pbxproj diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to apps/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to apps/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to apps/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to apps/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/apps/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to apps/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to apps/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to apps/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/example/ios/Runner/AppDelegate.swift b/apps/example/ios/Runner/AppDelegate.swift similarity index 100% rename from example/ios/Runner/AppDelegate.swift rename to apps/example/ios/Runner/AppDelegate.swift diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/apps/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to apps/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/apps/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to apps/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/apps/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to apps/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/apps/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to apps/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/apps/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to apps/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/apps/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to apps/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/apps/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/Main.storyboard rename to apps/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/example/ios/Runner/Info.plist b/apps/example/ios/Runner/Info.plist similarity index 100% rename from example/ios/Runner/Info.plist rename to apps/example/ios/Runner/Info.plist diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/apps/example/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from example/ios/Runner/Runner-Bridging-Header.h rename to apps/example/ios/Runner/Runner-Bridging-Header.h diff --git a/example/ios/RunnerTests/RunnerTests.swift b/apps/example/ios/RunnerTests/RunnerTests.swift similarity index 100% rename from example/ios/RunnerTests/RunnerTests.swift rename to apps/example/ios/RunnerTests/RunnerTests.swift diff --git a/example/lib/main.dart b/apps/example/lib/main.dart similarity index 99% rename from example/lib/main.dart rename to apps/example/lib/main.dart index 922b55e8..74250c5b 100644 --- a/example/lib/main.dart +++ b/apps/example/lib/main.dart @@ -6,6 +6,7 @@ import 'dart:io' show Platform; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_tts/flutter_tts.dart'; +import 'package:flutter_tts_android/flutter_tts_android.dart'; extension on Voice { String get displayName { @@ -201,7 +202,7 @@ class _MyAppState extends State { } Future _setAwaitOptions() async { - await flutterTts.awaitSpeakCompletion(true); + await flutterTts.awaitSpeakCompletion(awaitCompletion: true); } Future _stop() async { diff --git a/example/macos/.gitignore b/apps/example/macos/.gitignore similarity index 100% rename from example/macos/.gitignore rename to apps/example/macos/.gitignore diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/apps/example/macos/Flutter/Flutter-Debug.xcconfig similarity index 100% rename from example/macos/Flutter/Flutter-Debug.xcconfig rename to apps/example/macos/Flutter/Flutter-Debug.xcconfig diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/apps/example/macos/Flutter/Flutter-Release.xcconfig similarity index 100% rename from example/macos/Flutter/Flutter-Release.xcconfig rename to apps/example/macos/Flutter/Flutter-Release.xcconfig diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/apps/example/macos/Flutter/GeneratedPluginRegistrant.swift similarity index 90% rename from example/macos/Flutter/GeneratedPluginRegistrant.swift rename to apps/example/macos/Flutter/GeneratedPluginRegistrant.swift index 1e6ac53b..53005524 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/apps/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,7 @@ import FlutterMacOS import Foundation -import flutter_tts +import flutter_tts_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) diff --git a/example/macos/Podfile b/apps/example/macos/Podfile similarity index 100% rename from example/macos/Podfile rename to apps/example/macos/Podfile diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/apps/example/macos/Runner.xcodeproj/project.pbxproj similarity index 100% rename from example/macos/Runner.xcodeproj/project.pbxproj rename to apps/example/macos/Runner.xcodeproj/project.pbxproj diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to apps/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to apps/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/apps/example/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/macos/Runner.xcworkspace/contents.xcworkspacedata rename to apps/example/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to apps/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/macos/Runner/AppDelegate.swift b/apps/example/macos/Runner/AppDelegate.swift similarity index 100% rename from example/macos/Runner/AppDelegate.swift rename to apps/example/macos/Runner/AppDelegate.swift diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png similarity index 100% rename from example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png rename to apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png similarity index 100% rename from example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png similarity index 100% rename from example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png rename to apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png similarity index 100% rename from example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png rename to apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png similarity index 100% rename from example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png rename to apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png similarity index 100% rename from example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png rename to apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png similarity index 100% rename from example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png rename to apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/apps/example/macos/Runner/Base.lproj/MainMenu.xib similarity index 100% rename from example/macos/Runner/Base.lproj/MainMenu.xib rename to apps/example/macos/Runner/Base.lproj/MainMenu.xib diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/apps/example/macos/Runner/Configs/AppInfo.xcconfig similarity index 100% rename from example/macos/Runner/Configs/AppInfo.xcconfig rename to apps/example/macos/Runner/Configs/AppInfo.xcconfig diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/apps/example/macos/Runner/Configs/Debug.xcconfig similarity index 100% rename from example/macos/Runner/Configs/Debug.xcconfig rename to apps/example/macos/Runner/Configs/Debug.xcconfig diff --git a/example/macos/Runner/Configs/Release.xcconfig b/apps/example/macos/Runner/Configs/Release.xcconfig similarity index 100% rename from example/macos/Runner/Configs/Release.xcconfig rename to apps/example/macos/Runner/Configs/Release.xcconfig diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/apps/example/macos/Runner/Configs/Warnings.xcconfig similarity index 100% rename from example/macos/Runner/Configs/Warnings.xcconfig rename to apps/example/macos/Runner/Configs/Warnings.xcconfig diff --git a/example/macos/Runner/DebugProfile.entitlements b/apps/example/macos/Runner/DebugProfile.entitlements similarity index 100% rename from example/macos/Runner/DebugProfile.entitlements rename to apps/example/macos/Runner/DebugProfile.entitlements diff --git a/example/macos/Runner/Info.plist b/apps/example/macos/Runner/Info.plist similarity index 100% rename from example/macos/Runner/Info.plist rename to apps/example/macos/Runner/Info.plist diff --git a/example/macos/Runner/MainFlutterWindow.swift b/apps/example/macos/Runner/MainFlutterWindow.swift similarity index 100% rename from example/macos/Runner/MainFlutterWindow.swift rename to apps/example/macos/Runner/MainFlutterWindow.swift diff --git a/example/macos/Runner/Release.entitlements b/apps/example/macos/Runner/Release.entitlements similarity index 100% rename from example/macos/Runner/Release.entitlements rename to apps/example/macos/Runner/Release.entitlements diff --git a/example/macos/RunnerTests/RunnerTests.swift b/apps/example/macos/RunnerTests/RunnerTests.swift similarity index 100% rename from example/macos/RunnerTests/RunnerTests.swift rename to apps/example/macos/RunnerTests/RunnerTests.swift diff --git a/example/pubspec.yaml b/apps/example/pubspec.yaml similarity index 94% rename from example/pubspec.yaml rename to apps/example/pubspec.yaml index c77704cf..1b473dc0 100644 --- a/example/pubspec.yaml +++ b/apps/example/pubspec.yaml @@ -2,10 +2,10 @@ name: flutter_tts_example description: Demonstrates how to use the flutter_tts plugin. version: 0.0.1+1 publish_to: none +resolution: workspace environment: - sdk: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" + sdk: ^3.9.0 dependencies: flutter: @@ -14,13 +14,13 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.1+1 - flutter_tts: - path: ../ + flutter_tts: ^5.0.0 + flutter_tts_android: ^5.0.0 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec diff --git a/example/test/widget_test.dart b/apps/example/test/widget_test.dart similarity index 100% rename from example/test/widget_test.dart rename to apps/example/test/widget_test.dart diff --git a/example/web/favicon.png b/apps/example/web/favicon.png similarity index 100% rename from example/web/favicon.png rename to apps/example/web/favicon.png diff --git a/example/web/icons/Icon-192.png b/apps/example/web/icons/Icon-192.png similarity index 100% rename from example/web/icons/Icon-192.png rename to apps/example/web/icons/Icon-192.png diff --git a/example/web/icons/Icon-512.png b/apps/example/web/icons/Icon-512.png similarity index 100% rename from example/web/icons/Icon-512.png rename to apps/example/web/icons/Icon-512.png diff --git a/example/web/index.html b/apps/example/web/index.html similarity index 100% rename from example/web/index.html rename to apps/example/web/index.html diff --git a/example/web/manifest.json b/apps/example/web/manifest.json similarity index 100% rename from example/web/manifest.json rename to apps/example/web/manifest.json diff --git a/example/windows/.gitignore b/apps/example/windows/.gitignore similarity index 100% rename from example/windows/.gitignore rename to apps/example/windows/.gitignore diff --git a/example/windows/CMakeLists.txt b/apps/example/windows/CMakeLists.txt similarity index 100% rename from example/windows/CMakeLists.txt rename to apps/example/windows/CMakeLists.txt diff --git a/example/windows/flutter/CMakeLists.txt b/apps/example/windows/flutter/CMakeLists.txt similarity index 100% rename from example/windows/flutter/CMakeLists.txt rename to apps/example/windows/flutter/CMakeLists.txt diff --git a/example/windows/runner/CMakeLists.txt b/apps/example/windows/runner/CMakeLists.txt similarity index 100% rename from example/windows/runner/CMakeLists.txt rename to apps/example/windows/runner/CMakeLists.txt diff --git a/example/windows/runner/Runner.rc b/apps/example/windows/runner/Runner.rc similarity index 100% rename from example/windows/runner/Runner.rc rename to apps/example/windows/runner/Runner.rc diff --git a/example/windows/runner/flutter_window.cpp b/apps/example/windows/runner/flutter_window.cpp similarity index 100% rename from example/windows/runner/flutter_window.cpp rename to apps/example/windows/runner/flutter_window.cpp diff --git a/example/windows/runner/flutter_window.h b/apps/example/windows/runner/flutter_window.h similarity index 100% rename from example/windows/runner/flutter_window.h rename to apps/example/windows/runner/flutter_window.h diff --git a/example/windows/runner/main.cpp b/apps/example/windows/runner/main.cpp similarity index 100% rename from example/windows/runner/main.cpp rename to apps/example/windows/runner/main.cpp diff --git a/example/windows/runner/resource.h b/apps/example/windows/runner/resource.h similarity index 100% rename from example/windows/runner/resource.h rename to apps/example/windows/runner/resource.h diff --git a/example/windows/runner/resources/app_icon.ico b/apps/example/windows/runner/resources/app_icon.ico similarity index 100% rename from example/windows/runner/resources/app_icon.ico rename to apps/example/windows/runner/resources/app_icon.ico diff --git a/example/windows/runner/runner.exe.manifest b/apps/example/windows/runner/runner.exe.manifest similarity index 100% rename from example/windows/runner/runner.exe.manifest rename to apps/example/windows/runner/runner.exe.manifest diff --git a/example/windows/runner/utils.cpp b/apps/example/windows/runner/utils.cpp similarity index 100% rename from example/windows/runner/utils.cpp rename to apps/example/windows/runner/utils.cpp diff --git a/example/windows/runner/utils.h b/apps/example/windows/runner/utils.h similarity index 100% rename from example/windows/runner/utils.h rename to apps/example/windows/runner/utils.h diff --git a/example/windows/runner/win32_window.cpp b/apps/example/windows/runner/win32_window.cpp similarity index 100% rename from example/windows/runner/win32_window.cpp rename to apps/example/windows/runner/win32_window.cpp diff --git a/example/windows/runner/win32_window.h b/apps/example/windows/runner/win32_window.h similarity index 100% rename from example/windows/runner/win32_window.h rename to apps/example/windows/runner/win32_window.h diff --git a/example/winuwp/.gitignore b/apps/example/winuwp/.gitignore similarity index 100% rename from example/winuwp/.gitignore rename to apps/example/winuwp/.gitignore diff --git a/example/winuwp/CMakeLists.txt b/apps/example/winuwp/CMakeLists.txt similarity index 100% rename from example/winuwp/CMakeLists.txt rename to apps/example/winuwp/CMakeLists.txt diff --git a/example/winuwp/flutter/CMakeLists.txt b/apps/example/winuwp/flutter/CMakeLists.txt similarity index 100% rename from example/winuwp/flutter/CMakeLists.txt rename to apps/example/winuwp/flutter/CMakeLists.txt diff --git a/example/winuwp/flutter/flutter_windows.h b/apps/example/winuwp/flutter/flutter_windows.h similarity index 100% rename from example/winuwp/flutter/flutter_windows.h rename to apps/example/winuwp/flutter/flutter_windows.h diff --git a/example/winuwp/project_version b/apps/example/winuwp/project_version similarity index 100% rename from example/winuwp/project_version rename to apps/example/winuwp/project_version diff --git a/example/winuwp/runner_uwp/Assets/LargeTile.scale-100.png b/apps/example/winuwp/runner_uwp/Assets/LargeTile.scale-100.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/LargeTile.scale-100.png rename to apps/example/winuwp/runner_uwp/Assets/LargeTile.scale-100.png diff --git a/example/winuwp/runner_uwp/Assets/LargeTile.scale-125.png b/apps/example/winuwp/runner_uwp/Assets/LargeTile.scale-125.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/LargeTile.scale-125.png rename to apps/example/winuwp/runner_uwp/Assets/LargeTile.scale-125.png diff --git a/example/winuwp/runner_uwp/Assets/LargeTile.scale-150.png b/apps/example/winuwp/runner_uwp/Assets/LargeTile.scale-150.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/LargeTile.scale-150.png rename to apps/example/winuwp/runner_uwp/Assets/LargeTile.scale-150.png diff --git a/example/winuwp/runner_uwp/Assets/LargeTile.scale-200.png b/apps/example/winuwp/runner_uwp/Assets/LargeTile.scale-200.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/LargeTile.scale-200.png rename to apps/example/winuwp/runner_uwp/Assets/LargeTile.scale-200.png diff --git a/example/winuwp/runner_uwp/Assets/LargeTile.scale-400.png b/apps/example/winuwp/runner_uwp/Assets/LargeTile.scale-400.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/LargeTile.scale-400.png rename to apps/example/winuwp/runner_uwp/Assets/LargeTile.scale-400.png diff --git a/example/winuwp/runner_uwp/Assets/LockScreenLogo.scale-200.png b/apps/example/winuwp/runner_uwp/Assets/LockScreenLogo.scale-200.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/LockScreenLogo.scale-200.png rename to apps/example/winuwp/runner_uwp/Assets/LockScreenLogo.scale-200.png diff --git a/example/winuwp/runner_uwp/Assets/SmallTile.scale-100.png b/apps/example/winuwp/runner_uwp/Assets/SmallTile.scale-100.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/SmallTile.scale-100.png rename to apps/example/winuwp/runner_uwp/Assets/SmallTile.scale-100.png diff --git a/example/winuwp/runner_uwp/Assets/SmallTile.scale-125.png b/apps/example/winuwp/runner_uwp/Assets/SmallTile.scale-125.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/SmallTile.scale-125.png rename to apps/example/winuwp/runner_uwp/Assets/SmallTile.scale-125.png diff --git a/example/winuwp/runner_uwp/Assets/SmallTile.scale-150.png b/apps/example/winuwp/runner_uwp/Assets/SmallTile.scale-150.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/SmallTile.scale-150.png rename to apps/example/winuwp/runner_uwp/Assets/SmallTile.scale-150.png diff --git a/example/winuwp/runner_uwp/Assets/SmallTile.scale-200.png b/apps/example/winuwp/runner_uwp/Assets/SmallTile.scale-200.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/SmallTile.scale-200.png rename to apps/example/winuwp/runner_uwp/Assets/SmallTile.scale-200.png diff --git a/example/winuwp/runner_uwp/Assets/SmallTile.scale-400.png b/apps/example/winuwp/runner_uwp/Assets/SmallTile.scale-400.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/SmallTile.scale-400.png rename to apps/example/winuwp/runner_uwp/Assets/SmallTile.scale-400.png diff --git a/example/winuwp/runner_uwp/Assets/SplashScreen.scale-100.png b/apps/example/winuwp/runner_uwp/Assets/SplashScreen.scale-100.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/SplashScreen.scale-100.png rename to apps/example/winuwp/runner_uwp/Assets/SplashScreen.scale-100.png diff --git a/example/winuwp/runner_uwp/Assets/SplashScreen.scale-125.png b/apps/example/winuwp/runner_uwp/Assets/SplashScreen.scale-125.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/SplashScreen.scale-125.png rename to apps/example/winuwp/runner_uwp/Assets/SplashScreen.scale-125.png diff --git a/example/winuwp/runner_uwp/Assets/SplashScreen.scale-150.png b/apps/example/winuwp/runner_uwp/Assets/SplashScreen.scale-150.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/SplashScreen.scale-150.png rename to apps/example/winuwp/runner_uwp/Assets/SplashScreen.scale-150.png diff --git a/example/winuwp/runner_uwp/Assets/SplashScreen.scale-200.png b/apps/example/winuwp/runner_uwp/Assets/SplashScreen.scale-200.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/SplashScreen.scale-200.png rename to apps/example/winuwp/runner_uwp/Assets/SplashScreen.scale-200.png diff --git a/example/winuwp/runner_uwp/Assets/SplashScreen.scale-400.png b/apps/example/winuwp/runner_uwp/Assets/SplashScreen.scale-400.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/SplashScreen.scale-400.png rename to apps/example/winuwp/runner_uwp/Assets/SplashScreen.scale-400.png diff --git a/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-100.png b/apps/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-100.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-100.png rename to apps/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-100.png diff --git a/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-125.png b/apps/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-125.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-125.png rename to apps/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-125.png diff --git a/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-150.png b/apps/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-150.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-150.png rename to apps/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-150.png diff --git a/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-200.png b/apps/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-200.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-200.png rename to apps/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-200.png diff --git a/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-400.png b/apps/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-400.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-400.png rename to apps/example/winuwp/runner_uwp/Assets/Square150x150Logo.scale-400.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-16.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-16.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-16.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-256.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-256.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-256.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-32.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-32.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-32.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-48.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-48.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.altform-unplated_targetsize-48.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-100.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-100.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-100.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-100.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-125.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-125.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-125.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-125.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-150.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-150.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-150.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-150.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-200.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-200.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-200.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-200.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-400.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-400.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-400.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.scale-400.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-16.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-16.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-16.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-16.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-24.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-24.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-24.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-24.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-24_altform-unplated.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-24_altform-unplated.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-24_altform-unplated.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-256.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-256.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-256.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-256.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-32.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-32.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-32.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-32.png diff --git a/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-48.png b/apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-48.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-48.png rename to apps/example/winuwp/runner_uwp/Assets/Square44x44Logo.targetsize-48.png diff --git a/example/winuwp/runner_uwp/Assets/StoreLogo.png b/apps/example/winuwp/runner_uwp/Assets/StoreLogo.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/StoreLogo.png rename to apps/example/winuwp/runner_uwp/Assets/StoreLogo.png diff --git a/example/winuwp/runner_uwp/Assets/StoreLogo.scale-100.png b/apps/example/winuwp/runner_uwp/Assets/StoreLogo.scale-100.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/StoreLogo.scale-100.png rename to apps/example/winuwp/runner_uwp/Assets/StoreLogo.scale-100.png diff --git a/example/winuwp/runner_uwp/Assets/StoreLogo.scale-125.png b/apps/example/winuwp/runner_uwp/Assets/StoreLogo.scale-125.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/StoreLogo.scale-125.png rename to apps/example/winuwp/runner_uwp/Assets/StoreLogo.scale-125.png diff --git a/example/winuwp/runner_uwp/Assets/StoreLogo.scale-150.png b/apps/example/winuwp/runner_uwp/Assets/StoreLogo.scale-150.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/StoreLogo.scale-150.png rename to apps/example/winuwp/runner_uwp/Assets/StoreLogo.scale-150.png diff --git a/example/winuwp/runner_uwp/Assets/StoreLogo.scale-200.png b/apps/example/winuwp/runner_uwp/Assets/StoreLogo.scale-200.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/StoreLogo.scale-200.png rename to apps/example/winuwp/runner_uwp/Assets/StoreLogo.scale-200.png diff --git a/example/winuwp/runner_uwp/Assets/StoreLogo.scale-400.png b/apps/example/winuwp/runner_uwp/Assets/StoreLogo.scale-400.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/StoreLogo.scale-400.png rename to apps/example/winuwp/runner_uwp/Assets/StoreLogo.scale-400.png diff --git a/example/winuwp/runner_uwp/Assets/Wide310x150Logo.scale-200.png b/apps/example/winuwp/runner_uwp/Assets/Wide310x150Logo.scale-200.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/Wide310x150Logo.scale-200.png rename to apps/example/winuwp/runner_uwp/Assets/Wide310x150Logo.scale-200.png diff --git a/example/winuwp/runner_uwp/Assets/WideTile.scale-100.png b/apps/example/winuwp/runner_uwp/Assets/WideTile.scale-100.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/WideTile.scale-100.png rename to apps/example/winuwp/runner_uwp/Assets/WideTile.scale-100.png diff --git a/example/winuwp/runner_uwp/Assets/WideTile.scale-125.png b/apps/example/winuwp/runner_uwp/Assets/WideTile.scale-125.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/WideTile.scale-125.png rename to apps/example/winuwp/runner_uwp/Assets/WideTile.scale-125.png diff --git a/example/winuwp/runner_uwp/Assets/WideTile.scale-150.png b/apps/example/winuwp/runner_uwp/Assets/WideTile.scale-150.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/WideTile.scale-150.png rename to apps/example/winuwp/runner_uwp/Assets/WideTile.scale-150.png diff --git a/example/winuwp/runner_uwp/Assets/WideTile.scale-200.png b/apps/example/winuwp/runner_uwp/Assets/WideTile.scale-200.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/WideTile.scale-200.png rename to apps/example/winuwp/runner_uwp/Assets/WideTile.scale-200.png diff --git a/example/winuwp/runner_uwp/Assets/WideTile.scale-400.png b/apps/example/winuwp/runner_uwp/Assets/WideTile.scale-400.png similarity index 100% rename from example/winuwp/runner_uwp/Assets/WideTile.scale-400.png rename to apps/example/winuwp/runner_uwp/Assets/WideTile.scale-400.png diff --git a/example/winuwp/runner_uwp/CMakeLists.txt b/apps/example/winuwp/runner_uwp/CMakeLists.txt similarity index 100% rename from example/winuwp/runner_uwp/CMakeLists.txt rename to apps/example/winuwp/runner_uwp/CMakeLists.txt diff --git a/example/winuwp/runner_uwp/CMakeSettings.json b/apps/example/winuwp/runner_uwp/CMakeSettings.json similarity index 100% rename from example/winuwp/runner_uwp/CMakeSettings.json rename to apps/example/winuwp/runner_uwp/CMakeSettings.json diff --git a/example/winuwp/runner_uwp/Windows_TemporaryKey.pfx b/apps/example/winuwp/runner_uwp/Windows_TemporaryKey.pfx similarity index 100% rename from example/winuwp/runner_uwp/Windows_TemporaryKey.pfx rename to apps/example/winuwp/runner_uwp/Windows_TemporaryKey.pfx diff --git a/example/winuwp/runner_uwp/appxmanifest.in b/apps/example/winuwp/runner_uwp/appxmanifest.in similarity index 100% rename from example/winuwp/runner_uwp/appxmanifest.in rename to apps/example/winuwp/runner_uwp/appxmanifest.in diff --git a/example/winuwp/runner_uwp/flutter_frameworkview.cpp b/apps/example/winuwp/runner_uwp/flutter_frameworkview.cpp similarity index 100% rename from example/winuwp/runner_uwp/flutter_frameworkview.cpp rename to apps/example/winuwp/runner_uwp/flutter_frameworkview.cpp diff --git a/example/winuwp/runner_uwp/main.cpp b/apps/example/winuwp/runner_uwp/main.cpp similarity index 100% rename from example/winuwp/runner_uwp/main.cpp rename to apps/example/winuwp/runner_uwp/main.cpp diff --git a/example/winuwp/runner_uwp/resources.pri b/apps/example/winuwp/runner_uwp/resources.pri similarity index 100% rename from example/winuwp/runner_uwp/resources.pri rename to apps/example/winuwp/runner_uwp/resources.pri diff --git a/lib/flutter_tts.dart b/lib/flutter_tts.dart deleted file mode 100644 index 4ed60652..00000000 --- a/lib/flutter_tts.dart +++ /dev/null @@ -1,5 +0,0 @@ -export 'src/flutter_tts.dart'; -export 'src/flutter_tts_native.dart' - if (dart.library.html) 'src/flutter_tts_web.dart'; -export 'src/flutter_tts_platform_interface.dart'; -export 'src/messages.g.dart'; diff --git a/lib/src/flutter_tts.dart b/lib/src/flutter_tts.dart deleted file mode 100644 index f397db40..00000000 --- a/lib/src/flutter_tts.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:flutter_tts/src/flutter_tts_platform_interface.dart'; - -class FlutterTts { - static FlutterTtsPlatform get platform => FlutterTtsPlatform.instance; -} diff --git a/lib/src/flutter_tts_ios.dart b/lib/src/flutter_tts_ios.dart deleted file mode 100644 index b1be187d..00000000 --- a/lib/src/flutter_tts_ios.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:flutter_tts/src/flutter_tts_method_channel.dart'; -import 'package:flutter_tts/src/flutter_tts_platform_interface.dart'; -import 'package:flutter_tts/src/messages.g.dart'; - -class FlutterTtsIos extends FlutterTtsMethodChannel { - static void registerWith() { - FlutterTtsPlatform.instance = FlutterTtsIos(); - } - - final IosTtsHostApi iosHostApi = IosTtsHostApi(); - - /// [Future] which sets synthesize to file's future to return on completion of the synthesize - /// ***Android, iOS, and macOS supported only*** - Future> awaitSynthCompletion( - bool awaitCompletion, - ) async { - try { - return ResultDart.success( - await iosHostApi.awaitSynthCompletion(awaitCompletion), - ); - } on Exception catch (e) { - return ResultDart.error(e); - } - } - - /// [Future] which invokes the platform specific method for synthesizeToFile - Future> synthesizeToFile( - String text, - String fileName, [ - bool isFullPath = false, - ]) async { - try { - return ResultDart.success( - await iosHostApi.synthesizeToFile(text, fileName, isFullPath), - ); - } on Exception catch (e) { - return ResultDart.error(e); - } - } - - /// [Future] which invokes the platform specific method for shared instance - /// ***iOS supported only*** - Future> setSharedInstance(bool sharedSession) async { - try { - return ResultDart.success( - await iosHostApi.setSharedInstance(sharedSession), - ); - } on Exception catch (e) { - return ResultDart.error(e); - } - } - - /// [Future] which invokes the platform specific method for setting the autoStopSharedSession - /// default value is true - /// *** iOS, and macOS supported only*** - Future> autoStopSharedSession(bool autoStop) async { - try { - return ResultDart.success( - await iosHostApi.autoStopSharedSession(autoStop), - ); - } on Exception catch (e) { - return ResultDart.error(e); - } - } - - /// [Future] which invokes the platform specific method for setting audio category - /// ***Ios supported only*** - Future> setIosAudioCategory( - IosTextToSpeechAudioCategory category, - List options, { - IosTextToSpeechAudioMode mode = IosTextToSpeechAudioMode.defaultMode, - }) async { - try { - return ResultDart.success( - await iosHostApi.setIosAudioCategory(category, options, mode: mode), - ); - } on Exception catch (e) { - return ResultDart.error(e); - } - } - - Future> getSpeechRateValidRange() async { - try { - return ResultDart.success(await iosHostApi.getSpeechRateValidRange()); - } on Exception catch (e) { - return ResultDart.error(e); - } - } - - /// [Future] which invokes the platform specific method for isLanguageAvailable - /// Returns `true` or `false` - Future> isLanguageAvailable(String language) async { - try { - return ResultDart.success(await iosHostApi.isLanguageAvailable(language)); - } on Exception catch (e) { - return ResultDart.error(e); - } - } - - Future> setLanguange(String language) async { - try { - return ResultDart.success(await iosHostApi.setLanguange(language)); - } on Exception catch (e) { - return ResultDart.error(e); - } - } -} diff --git a/lib/src/flutter_tts_macos.dart b/lib/src/flutter_tts_macos.dart deleted file mode 100644 index 3a859c70..00000000 --- a/lib/src/flutter_tts_macos.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter_tts/src/flutter_tts_method_channel.dart'; -import 'package:flutter_tts/src/flutter_tts_platform_interface.dart'; -import 'package:flutter_tts/src/messages.g.dart'; - -class FlutterTtsMacos extends FlutterTtsMethodChannel { - static void registerWith() { - FlutterTtsPlatform.instance = FlutterTtsMacos(); - } - - final macosHostApi = MacosTtsHostApi(); - - /// [Future] which sets synthesize to file's future to return on completion of the synthesize - /// ***Android, iOS, and macOS supported only*** - Future> awaitSynthCompletion( - bool awaitCompletion, - ) async { - try { - return ResultDart.success( - await macosHostApi.awaitSynthCompletion(awaitCompletion), - ); - } on Exception catch (e) { - return ResultDart.error(e); - } - } - - /// [Future] which invokes the platform specific method for getting the speech rate valid range - /// ***iOS, and macOS supported only*** - Future> getSpeechRateValidRange() async { - try { - return ResultDart.success(await macosHostApi.getSpeechRateValidRange()); - } on Exception catch (e) { - return ResultDart.error(e); - } - } - - Future> setLanguange(String language) async { - try { - return ResultDart.success(await macosHostApi.setLanguange(language)); - } on Exception catch (e) { - return ResultDart.error(e); - } - } - - Future> isLanguageAvailable(String language) async { - try { - return ResultDart.success( - await macosHostApi.isLanguageAvailable(language), - ); - } on Exception catch (e) { - return ResultDart.error(e); - } - } -} diff --git a/lib/src/flutter_tts_native.dart b/lib/src/flutter_tts_native.dart deleted file mode 100644 index 599d51a5..00000000 --- a/lib/src/flutter_tts_native.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'package:flutter_tts/src/flutter_tts_android.dart'; -export 'package:flutter_tts/src/flutter_tts_ios.dart'; -export 'package:flutter_tts/src/flutter_tts_macos.dart'; -export 'package:flutter_tts/src/flutter_tts_method_channel.dart'; diff --git a/lib/src/flutter_tts_platform_interface.dart b/lib/src/flutter_tts_platform_interface.dart deleted file mode 100644 index 86316d30..00000000 --- a/lib/src/flutter_tts_platform_interface.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_tts/src/flutter_tts_method_channel.dart'; -import 'package:flutter_tts/src/messages.g.dart'; -import 'package:multiple_result/multiple_result.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -typedef ResultDart = Result; -typedef SuccessDart = Success; -typedef FailureDart = Error; - -abstract class FlutterTtsPlatform extends PlatformInterface { - static const token = Object(); - - static FlutterTtsPlatform _instance = FlutterTtsMethodChannel(); - - /// The default instance of [FlutterTtsPlatform ] to use. - /// - /// Defaults to [MethodChannelFlutterSysFonts]. - static FlutterTtsPlatform get instance => _instance; - - /// Platform-specific implementations should set this with their own - /// platform-specific class that extends [FlutterTtsPlatform ] when - /// they register themselves. - static set instance(FlutterTtsPlatform instance) { - PlatformInterface.verifyToken(instance, FlutterTtsPlatform.token); - _instance = instance; - } - - VoidCallback? onSpeakStart; - VoidCallback? onSpeakComplete; - VoidCallback? onSpeakPause; - VoidCallback? onSpeakResume; - VoidCallback? onSpeakCancel; - ValueChanged? onSpeakError; - ValueChanged? onSpeakProgress; - - VoidCallback? onSynthStart; - VoidCallback? onSynthComplete; - ValueChanged? onSynthError; - - FlutterTtsPlatform() : super(token: token); - - /// [Future] which sets speak's future to return on completion of the utterance - Future> awaitSpeakCompletion(bool awaitCompletion); - - /// [Future] which invokes the platform specific method for speaking - Future> speak(String text, {bool focus = false}); - - /// [Future] which invokes the platform specific method for pause - Future> pause(); - - /// [Future] which invokes the platform specific method for stop - Future> stop(); - - /// [Future] which invokes the platform specific method for setSpeechRate - /// Allowed values are in the range from 0.0 (slowest) to 1.0 (fastest) - Future> setSpeechRate(double rate); - - /// [Future] which invokes the platform specific method for setVolume - /// Allowed values are in the range from 0.0 (silent) to 1.0 (loudest) - Future> setVolume(double volume); - - /// [Future] which invokes the platform specific method for setPitch - /// 1.0 is default and ranges from .5 to 2.0 - Future> setPitch(double pitch); - - Future>> getLanguages(); - - /// [Future] which invokes the platform specific method for getVoices - /// Returns a `List` of `Maps` containing a voice name and locale - /// For iOS specifically, it also includes quality, gender, and identifier - /// ***Android, iOS, and macOS supported only*** - Future>> getVoices(); - - /// [Future] which invokes the platform specific method for setVoice - Future> setVoice(Voice voice); - - /// [Future] which resets the platform voice to the default - Future> clearVoice(); -} diff --git a/packages/.gitignore b/packages/.gitignore new file mode 100644 index 00000000..4aa0df8d --- /dev/null +++ b/packages/.gitignore @@ -0,0 +1,48 @@ +.DS_Store +.atom/ +.idea/ +.vscode/ + +.packages +.pub/ +.dart_tool/ +pubspec.lock +flutter_export_environment.sh +coverage/ + +Podfile.lock +Pods/ +.symlinks/ +**/Flutter/App.framework/ +**/Flutter/ephemeral/ +**/Flutter/Flutter.podspec +**/Flutter/Flutter.framework/ +**/Flutter/Generated.xcconfig +**/Flutter/flutter_assets/ + +ServiceDefinitions.json +xcuserdata/ +**/DerivedData/ + +local.properties +keystore.properties +.gradle/ +gradlew +gradlew.bat +gradle-wrapper.jar +.flutter-plugins-dependencies +*.iml + +generated_plugin_registrant.cc +generated_plugin_registrant.h +generated_plugin_registrant.dart +GeneratedPluginRegistrant.java +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m +GeneratedPluginRegistrant.swift +build/ +.flutter-plugins + +.project +.classpath +.settings \ No newline at end of file diff --git a/packages/flutter_tts/README.md b/packages/flutter_tts/README.md new file mode 100644 index 00000000..957445bf --- /dev/null +++ b/packages/flutter_tts/README.md @@ -0,0 +1,26 @@ +# flutter_tts + +[![Very Good Ventures][logo_white]][very_good_ventures_link_dark] +[![Very Good Ventures][logo_black]][very_good_ventures_link_light] + +Developed with 💙 by [Very Good Ventures][very_good_ventures_link] 🦄 + +![coverage][coverage_badge] +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +A Very Good Flutter Federated Plugin created by the [Very Good Ventures Team][very_good_ventures_link]. + +Generated by the [Very Good CLI][very_good_cli_link] 🤖 + +[coverage_badge]: coverage_badge.svg +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only +[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis +[very_good_cli_link]: https://github.com/VeryGoodOpenSource/very_good_cli +[very_good_ventures_link]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core +[very_good_ventures_link_dark]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core#gh-dark-mode-only +[very_good_ventures_link_light]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core#gh-light-mode-only diff --git a/packages/flutter_tts/analysis_options.yaml b/packages/flutter_tts/analysis_options.yaml new file mode 100644 index 00000000..4c56a11a --- /dev/null +++ b/packages/flutter_tts/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:very_good_analysis/analysis_options.yaml +analyzer: + errors: + lines_longer_than_80_chars: ignore diff --git a/packages/flutter_tts/coverage_badge.svg b/packages/flutter_tts/coverage_badge.svg new file mode 100644 index 00000000..499e98ce --- /dev/null +++ b/packages/flutter_tts/coverage_badge.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + coverage + coverage + 100% + 100% + + diff --git a/packages/flutter_tts/lib/flutter_tts.dart b/packages/flutter_tts/lib/flutter_tts.dart new file mode 100644 index 00000000..1c98a044 --- /dev/null +++ b/packages/flutter_tts/lib/flutter_tts.dart @@ -0,0 +1,16 @@ +import 'package:flutter_tts_platform_interface/flutter_tts_platform_interface.dart'; + +export 'package:flutter_tts_platform_interface/flutter_tts_platform_interface.dart'; + +/// app facing class +abstract class FlutterTts { + /// get the instance of FlutterTtsPlatform + static FlutterTtsPlatform get platform => FlutterTtsPlatform.instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [FlutterTtsPlatform ] when + /// they register themselves. + static set platform(FlutterTtsPlatform instance) { + FlutterTtsPlatform.instance = instance; + } +} diff --git a/packages/flutter_tts/pubspec.yaml b/packages/flutter_tts/pubspec.yaml new file mode 100644 index 00000000..82887b4d --- /dev/null +++ b/packages/flutter_tts/pubspec.yaml @@ -0,0 +1,40 @@ +name: flutter_tts +description: A flutter plugin for Text to Speech. This plugin is supported on iOS, macOS, Android, Web, & Windows. +version: 5.0.0 +homepage: https://github.com/dlutton/flutter_tts +resolution: workspace + +environment: + sdk: ^3.9.0 + +flutter: + plugin: + platforms: + android: + default_package: flutter_tts_android + ios: + default_package: flutter_tts_ios + macos: + default_package: flutter_tts_macos + web: + default_package: flutter_tts_web + windows: + default_package: flutter_tts_windows + +dependencies: + flutter: + sdk: flutter + flutter_tts_android: ^5.0.0 + flutter_tts_ios: ^5.0.0 + flutter_tts_macos: ^5.0.0 + flutter_tts_platform_interface: ^5.0.0 + flutter_tts_web: ^5.0.0 + flutter_tts_windows: ^5.0.0 + multiple_result: ^5.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^1.0.4 + plugin_platform_interface: ^2.1.8 + very_good_analysis: ^10.0.0 diff --git a/packages/flutter_tts_android/README.md b/packages/flutter_tts_android/README.md new file mode 100644 index 00000000..987134c3 --- /dev/null +++ b/packages/flutter_tts_android/README.md @@ -0,0 +1,14 @@ +# flutter_tts_android + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] + +The Android implementation of `flutter_tts`. + +## Usage + +This package is [endorsed][endorsed_link], which means you can simply use `flutter_tts` +normally. This package will be automatically included in your app when you do. + +[endorsed_link]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/flutter_tts_android/analysis_options.yaml b/packages/flutter_tts_android/analysis_options.yaml new file mode 100644 index 00000000..4c56a11a --- /dev/null +++ b/packages/flutter_tts_android/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:very_good_analysis/analysis_options.yaml +analyzer: + errors: + lines_longer_than_80_chars: ignore diff --git a/android/.gitignore b/packages/flutter_tts_android/android/.gitignore similarity index 100% rename from android/.gitignore rename to packages/flutter_tts_android/android/.gitignore diff --git a/android/build.gradle b/packages/flutter_tts_android/android/build.gradle similarity index 100% rename from android/build.gradle rename to packages/flutter_tts_android/android/build.gradle diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_tts_android/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from android/gradle/wrapper/gradle-wrapper.properties rename to packages/flutter_tts_android/android/gradle/wrapper/gradle-wrapper.properties diff --git a/android/settings.gradle b/packages/flutter_tts_android/android/settings.gradle similarity index 100% rename from android/settings.gradle rename to packages/flutter_tts_android/android/settings.gradle diff --git a/android/src/main/AndroidManifest.xml b/packages/flutter_tts_android/android/src/main/AndroidManifest.xml similarity index 100% rename from android/src/main/AndroidManifest.xml rename to packages/flutter_tts_android/android/src/main/AndroidManifest.xml diff --git a/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt b/packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt similarity index 96% rename from android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt rename to packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt index 68f5a4e2..44f92023 100644 --- a/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt +++ b/packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt @@ -95,14 +95,20 @@ class FlutterTtsPlugin : FlutterPlugin, TtsHostApi, AndroidTtsHostApi { object : UtteranceProgressListener() { override fun onStart(utteranceId: String) { if (utteranceId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) { - flutterApi?.onSynthStartCb { } + handler?.post { + flutterApi?.onSynthStartCb { } + } } else { if (isPaused) { - flutterApi?.onSpeakResumeCb { } + handler?.post { + flutterApi?.onSpeakResumeCb { } + } isPaused = false } else { Log.d(tag, "Utterance ID has started: $utteranceId") - flutterApi?.onSpeakStartCb { } + handler?.post { + flutterApi?.onSpeakStartCb { } + } } } if (Build.VERSION.SDK_INT < 26) { @@ -118,13 +124,19 @@ class FlutterTtsPlugin : FlutterPlugin, TtsHostApi, AndroidTtsHostApi { if (awaitSynthCompletion) { synthCompletion(1) } - flutterApi?.onSynthCompleteCb { } + + handler?.post { + flutterApi?.onSynthCompleteCb { } + } } else { Log.d(tag, "Utterance ID has completed: $utteranceId") if (awaitSpeakCompletion && queueMode == TextToSpeech.QUEUE_FLUSH) { speakCompletion(1) } - flutterApi?.onSpeakCompleteCb { } + + handler?.post { + flutterApi?.onSpeakCompleteCb { } + } } lastProgress = 0 pauseText = null @@ -140,9 +152,13 @@ class FlutterTtsPlugin : FlutterPlugin, TtsHostApi, AndroidTtsHostApi { speaking = false } if (isPaused) { - flutterApi?.onSpeakPauseCb { } + handler?.post { + flutterApi?.onSpeakPauseCb { } + } } else { - flutterApi?.onSpeakCancelCb { } + handler?.post { + flutterApi?.onSpeakCancelCb { } + } } releaseAudioFocus() } @@ -157,7 +173,10 @@ class FlutterTtsPlugin : FlutterPlugin, TtsHostApi, AndroidTtsHostApi { end = endAt.toLong(), word = text.substring(startAt, endAt) ) - flutterApi?.onSpeakProgressCb(data) { } + + handler?.post { + flutterApi?.onSpeakProgressCb(data) { } + } } } } @@ -178,12 +197,18 @@ class FlutterTtsPlugin : FlutterPlugin, TtsHostApi, AndroidTtsHostApi { if (awaitSynthCompletion) { synth = false } - flutterApi?.onSynthErrorCb("Error from TextToSpeech (synth)") {} + + handler?.post { + flutterApi?.onSynthErrorCb("Error from TextToSpeech (synth)") {} + } } else { if (awaitSpeakCompletion) { speaking = false } - flutterApi?.onSpeakErrorCb("Error from TextToSpeech (speak)") {} + + handler?.post { + flutterApi?.onSpeakErrorCb("Error from TextToSpeech (speak)") {} + } } releaseAudioFocus() } @@ -194,12 +219,17 @@ class FlutterTtsPlugin : FlutterPlugin, TtsHostApi, AndroidTtsHostApi { if (awaitSynthCompletion) { synth = false } - flutterApi?.onSynthErrorCb("Error from TextToSpeech (synth) - $errorCode") {} + + handler?.post { + flutterApi?.onSynthErrorCb("Error from TextToSpeech (synth) - $errorCode") {} + } } else { if (awaitSpeakCompletion) { speaking = false } - flutterApi?.onSpeakErrorCb("Error from TextToSpeech (speak) - $errorCode") {} + handler?.post { + flutterApi?.onSpeakErrorCb("Error from TextToSpeech (speak) - $errorCode") {} + } } } } diff --git a/android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt b/packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt similarity index 100% rename from android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt rename to packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt diff --git a/lib/src/flutter_tts_android.dart b/packages/flutter_tts_android/lib/flutter_tts_android.dart similarity index 63% rename from lib/src/flutter_tts_android.dart rename to packages/flutter_tts_android/lib/flutter_tts_android.dart index ad6a773a..76d3d72c 100644 --- a/lib/src/flutter_tts_android.dart +++ b/packages/flutter_tts_android/lib/flutter_tts_android.dart @@ -1,22 +1,22 @@ -import 'package:flutter_tts/src/flutter_tts_method_channel.dart'; -import 'package:flutter_tts/src/flutter_tts_platform_interface.dart'; -import 'package:flutter_tts/src/messages.g.dart'; +import 'package:flutter_tts_platform_interface/flutter_tts_platform_interface.dart'; -class FlutterTtsAndroid extends FlutterTtsMethodChannel { +/// The Android implementation of [FlutterTtsPlatform]. +class FlutterTtsAndroid extends FlutterTtsPlatform with FlutterTtsPigeonMixin { + /// Registers this class as the default instance of [FlutterTtsPlatform] static void registerWith() { FlutterTtsPlatform.instance = FlutterTtsAndroid(); } - final androidHostApi = AndroidTtsHostApi(); + final _androidHostApi = AndroidTtsHostApi(); - /// [Future] which sets synthesize to file's future to return on completion of the synthesize - /// ***Android, iOS, and macOS supported only*** - Future> awaitSynthCompletion( - bool awaitCompletion, - ) async { + /// [Future] which sets synthesize to file's future to return + /// on completion of the synthesize + Future> awaitSynthCompletion({ + required bool awaitCompletion, + }) async { try { return ResultDart.success( - await androidHostApi.awaitSynthCompletion(awaitCompletion), + await _androidHostApi.awaitSynthCompletion(awaitCompletion), ); } on Exception catch (e) { return ResultDart.error(e); @@ -27,7 +27,9 @@ class FlutterTtsAndroid extends FlutterTtsMethodChannel { /// ***Android supported only*** Future> getMaxSpeechInputLength() async { try { - return ResultDart.success(await androidHostApi.getMaxSpeechInputLength()); + return ResultDart.success( + await _androidHostApi.getMaxSpeechInputLength(), + ); } on Exception catch (e) { return ResultDart.error(e); } @@ -37,7 +39,7 @@ class FlutterTtsAndroid extends FlutterTtsMethodChannel { /// ***Android supported only*** Future> setEngine(String engine) async { try { - return ResultDart.success(await androidHostApi.setEngine(engine)); + return ResultDart.success(await _androidHostApi.setEngine(engine)); } on Exception catch (e) { return ResultDart.error(e); } @@ -48,7 +50,7 @@ class FlutterTtsAndroid extends FlutterTtsMethodChannel { /// ***Android supported only*** Future>> getEngines() async { try { - return ResultDart.success(await androidHostApi.getEngines()); + return ResultDart.success(await _androidHostApi.getEngines()); } on Exception catch (e) { return ResultDart.error(e); } @@ -59,7 +61,7 @@ class FlutterTtsAndroid extends FlutterTtsMethodChannel { /// ***Android supported only *** Future> getDefaultEngine() async { try { - return ResultDart.success(await androidHostApi.getDefaultEngine()); + return ResultDart.success(await _androidHostApi.getDefaultEngine()); } on Exception catch (e) { return ResultDart.error(e); } @@ -70,7 +72,7 @@ class FlutterTtsAndroid extends FlutterTtsMethodChannel { /// ***Android supported only *** Future> getDefaultVoice() async { try { - return ResultDart.success(await androidHostApi.getDefaultVoice()); + return ResultDart.success(await _androidHostApi.getDefaultVoice()); } on Exception catch (e) { return ResultDart.error(e); } @@ -79,12 +81,13 @@ class FlutterTtsAndroid extends FlutterTtsMethodChannel { /// [Future] which invokes the platform specific method for synthesizeToFile Future> synthesizeToFile( String text, - String fileName, [ + String fileName, { bool isFullPath = false, - ]) async { + }) async { try { + ensureSetupTtsCallback(); return ResultDart.success( - await androidHostApi.synthesizeToFile(text, fileName, isFullPath), + await _androidHostApi.synthesizeToFile(text, fileName, isFullPath), ); } on Exception catch (e) { return ResultDart.error(e); @@ -97,7 +100,7 @@ class FlutterTtsAndroid extends FlutterTtsMethodChannel { Future> isLanguageInstalled(String language) async { try { return ResultDart.success( - await androidHostApi.isLanguageInstalled(language), + await _androidHostApi.isLanguageInstalled(language), ); } on Exception catch (e) { return ResultDart.error(e); @@ -109,7 +112,7 @@ class FlutterTtsAndroid extends FlutterTtsMethodChannel { Future> isLanguageAvailable(String language) async { try { return ResultDart.success( - await androidHostApi.isLanguageAvailable(language), + await _androidHostApi.isLanguageAvailable(language), ); } on Exception catch (e) { return ResultDart.error(e); @@ -124,54 +127,62 @@ class FlutterTtsAndroid extends FlutterTtsMethodChannel { ) async { try { return ResultDart.success( - await androidHostApi.areLanguagesInstalled(languages), + await _androidHostApi.areLanguagesInstalled(languages), ); } on Exception catch (e) { return ResultDart.error(e); } } - /// [Future] which invokes the platform specific method for getSpeechRateValidRange - /// Returns a `SpeechRateValidRange` object containing the minimum, normal, and maximum speech rate values for the current platform. - /// ***Android supported only*** + /// [Future] which invokes the platform specific method + /// for getSpeechRateValidRange + /// Returns a `SpeechRateValidRange` object containing the minimum, + /// normal, and maximum speech rate values for the current platform. Future> getSpeechRateValidRange() async { try { - return ResultDart.success(await androidHostApi.getSpeechRateValidRange()); + return ResultDart.success( + await _androidHostApi.getSpeechRateValidRange(), + ); } on Exception catch (e) { return ResultDart.error(e); } } /// [Future] which invokes the platform specific method for setSilence - /// 0 means start the utterance immediately. If the value is greater than zero a silence period in milliseconds is set according to the parameter - /// ***Android supported only*** + /// 0 means start the utterance immediately. + /// If the value is greater than zero + /// a silence period in milliseconds is set according to the parameter Future> setSilence(int timems) async { try { - return ResultDart.success(await androidHostApi.setSilence(timems)); + return ResultDart.success(await _androidHostApi.setSilence(timems)); } on Exception catch (e) { return ResultDart.error(e); } } /// [Future] which invokes the platform specific method for setQueueMode - /// 0 means QUEUE_FLUSH - Queue mode where all entries in the playback queue (media to be played and text to be synthesized) are dropped and replaced by the new entry. - /// Queues are flushed with respect to a given calling app. Entries in the queue from other calls are not discarded. - /// 1 means QUEUE_ADD - Queue mode where the new entry is added at the end of the playback queue. + /// 0 means QUEUE_FLUSH - Queue mode where all entries in the playback queue + /// (media to be played and text to be synthesized) + /// are dropped and replaced by the new entry. + /// Queues are flushed with respect to a given calling app. + /// Entries in the queue from other calls are not discarded. + /// 1 means QUEUE_ADD - Queue mode where the new entry is added + /// at the end of the playback queue. /// ***Android supported only*** Future> setQueueMode(int queueMode) async { try { - return ResultDart.success(await androidHostApi.setQueueMode(queueMode)); + return ResultDart.success(await _androidHostApi.setQueueMode(queueMode)); } on Exception catch (e) { return ResultDart.error(e); } } - /// [Future] which invokes the platform specific method for setAudioAttributesForNavigation - /// ***Android supported only*** + /// [Future] which invokes the platform specific method for + /// setAudioAttributesForNavigation Future> setAudioAttributesForNavigation() async { try { return ResultDart.success( - await androidHostApi.setAudioAttributesForNavigation(), + await _androidHostApi.setAudioAttributesForNavigation(), ); } on Exception catch (e) { return ResultDart.error(e); diff --git a/packages/flutter_tts_android/pubspec.yaml b/packages/flutter_tts_android/pubspec.yaml new file mode 100644 index 00000000..0b2fc804 --- /dev/null +++ b/packages/flutter_tts_android/pubspec.yaml @@ -0,0 +1,23 @@ +name: flutter_tts_android +description: A flutter plugin for Text to Speech. This plugin is supported on iOS, macOS, Android, Web, & Windows. +version: 5.0.0 +homepage: https://github.com/dlutton/flutter_tts +resolution: workspace + +environment: + sdk: ^3.9.0 + +dependencies: + flutter: + sdk: flutter + flutter_tts_platform_interface: ^5.0.0 + multiple_result: ^5.2.0 + +flutter: + plugin: + implements: flutter_tts + platforms: + android: + package: com.tundralabs.fluttertts + pluginClass: FlutterTtsPlugin + dartPluginClass: FlutterTtsAndroid diff --git a/packages/flutter_tts_ios/.gitignore b/packages/flutter_tts_ios/.gitignore new file mode 100644 index 00000000..ea8e0cbe --- /dev/null +++ b/packages/flutter_tts_ios/.gitignore @@ -0,0 +1,2 @@ +.build/ +.swiftpm/ \ No newline at end of file diff --git a/packages/flutter_tts_ios/README.md b/packages/flutter_tts_ios/README.md new file mode 100644 index 00000000..8c9fb551 --- /dev/null +++ b/packages/flutter_tts_ios/README.md @@ -0,0 +1,14 @@ +# flutter_tts_ios + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] + +The ios implementation of `flutter_tts`. + +## Usage + +This package is [endorsed][endorsed_link], which means you can simply use `flutter_tts` +normally. This package will be automatically included in your app when you do. + +[endorsed_link]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/flutter_tts_ios/analysis_options.yaml b/packages/flutter_tts_ios/analysis_options.yaml new file mode 100644 index 00000000..4c56a11a --- /dev/null +++ b/packages/flutter_tts_ios/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:very_good_analysis/analysis_options.yaml +analyzer: + errors: + lines_longer_than_80_chars: ignore diff --git a/ios/.gitignore b/packages/flutter_tts_ios/ios/.gitignore similarity index 100% rename from ios/.gitignore rename to packages/flutter_tts_ios/ios/.gitignore diff --git a/ios/Assets/.gitkeep b/packages/flutter_tts_ios/ios/Assets/.gitkeep similarity index 100% rename from ios/Assets/.gitkeep rename to packages/flutter_tts_ios/ios/Assets/.gitkeep diff --git a/ios/Classes/AudioCategory.swift b/packages/flutter_tts_ios/ios/Classes/AudioCategory.swift similarity index 100% rename from ios/Classes/AudioCategory.swift rename to packages/flutter_tts_ios/ios/Classes/AudioCategory.swift diff --git a/ios/Classes/AudioCategoryOptions.swift b/packages/flutter_tts_ios/ios/Classes/AudioCategoryOptions.swift similarity index 100% rename from ios/Classes/AudioCategoryOptions.swift rename to packages/flutter_tts_ios/ios/Classes/AudioCategoryOptions.swift diff --git a/ios/Classes/AudioModes.swift b/packages/flutter_tts_ios/ios/Classes/AudioModes.swift similarity index 100% rename from ios/Classes/AudioModes.swift rename to packages/flutter_tts_ios/ios/Classes/AudioModes.swift diff --git a/ios/Classes/SwiftFlutterTtsPlugin.swift b/packages/flutter_tts_ios/ios/Classes/SwiftFlutterTtsPlugin.swift similarity index 100% rename from ios/Classes/SwiftFlutterTtsPlugin.swift rename to packages/flutter_tts_ios/ios/Classes/SwiftFlutterTtsPlugin.swift diff --git a/ios/Classes/message.g.swift b/packages/flutter_tts_ios/ios/Classes/message.g.swift similarity index 100% rename from ios/Classes/message.g.swift rename to packages/flutter_tts_ios/ios/Classes/message.g.swift diff --git a/ios/flutter_tts.podspec b/packages/flutter_tts_ios/ios/flutter_tts_ios.podspec similarity index 94% rename from ios/flutter_tts.podspec rename to packages/flutter_tts_ios/ios/flutter_tts_ios.podspec index 5aab5b43..1cb87c4e 100644 --- a/ios/flutter_tts.podspec +++ b/packages/flutter_tts_ios/ios/flutter_tts_ios.podspec @@ -2,7 +2,7 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| - s.name = 'flutter_tts' + s.name = 'flutter_tts_ios' s.version = '0.0.1' s.summary = 'A flutter text to speech plugin.' s.description = <<-DESC diff --git a/packages/flutter_tts_ios/lib/flutter_tts_ios.dart b/packages/flutter_tts_ios/lib/flutter_tts_ios.dart new file mode 100644 index 00000000..16e83590 --- /dev/null +++ b/packages/flutter_tts_ios/lib/flutter_tts_ios.dart @@ -0,0 +1,119 @@ +import 'package:flutter_tts_platform_interface/flutter_tts_platform_interface.dart'; + +/// The iOS implementation of [FlutterTtsPlatform]. +class FlutterTtsIos extends FlutterTtsPlatform with FlutterTtsPigeonMixin { + /// Registers this class as the default instance of [FlutterTtsPlatform] + static void registerWith() { + FlutterTtsPlatform.instance = FlutterTtsIos(); + } + + final IosTtsHostApi _iosHostApi = IosTtsHostApi(); + + /// [Future] which sets synthesize to file's future to return + /// on completion of the synthesize + Future> awaitSynthCompletion({ + required bool awaitCompletion, + }) async { + try { + return ResultDart.success( + await _iosHostApi.awaitSynthCompletion(awaitCompletion), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for synthesizeToFile + Future> synthesizeToFile( + String text, + String fileName, { + bool isFullPath = false, + }) async { + try { + ensureSetupTtsCallback(); + return ResultDart.success( + await _iosHostApi.synthesizeToFile(text, fileName, isFullPath), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for shared instance + /// ***iOS supported only*** + Future> setSharedInstance({ + required bool sharedSession, + }) async { + try { + return ResultDart.success( + await _iosHostApi.setSharedInstance(sharedSession), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for + /// setting the autoStopSharedSession, default value is true + Future> autoStopSharedSession({ + required bool autoStop, + }) async { + try { + return ResultDart.success( + await _iosHostApi.autoStopSharedSession(autoStop), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method + /// for setting audio category + Future> setIosAudioCategory( + IosTextToSpeechAudioCategory category, + List options, { + IosTextToSpeechAudioMode mode = IosTextToSpeechAudioMode.defaultMode, + }) async { + try { + return ResultDart.success( + await _iosHostApi.setIosAudioCategory(category, options, mode: mode), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for + /// getting the speech rate valid range + Future> getSpeechRateValidRange() async { + try { + return ResultDart.success(await _iosHostApi.getSpeechRateValidRange()); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for + /// checking if the language is available, see also [setLanguange] + Future> isLanguageAvailable(String language) async { + try { + return ResultDart.success( + await _iosHostApi.isLanguageAvailable(language), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for + /// setting the language + /// Ios 9.0 or below does not support Voice selection, + /// use Language selection instead + Future> setLanguange(String language) async { + try { + return ResultDart.success(await _iosHostApi.setLanguange(language)); + } on Exception catch (e) { + return ResultDart.error(e); + } + } +} diff --git a/packages/flutter_tts_ios/pubspec.yaml b/packages/flutter_tts_ios/pubspec.yaml new file mode 100644 index 00000000..284d7542 --- /dev/null +++ b/packages/flutter_tts_ios/pubspec.yaml @@ -0,0 +1,28 @@ +name: flutter_tts_ios +description: A flutter plugin for Text to Speech. This plugin is supported on iOS, macOS, Android, Web, & Windows. +version: 5.0.0 +homepage: https://github.com/dlutton/flutter_tts +resolution: workspace + +environment: + sdk: ^3.9.0 + +dependencies: + flutter: + sdk: flutter + flutter_tts_platform_interface: ^5.0.0 + multiple_result: ^5.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.1.8 + very_good_analysis: ^10.0.0 + +flutter: + plugin: + implements: flutter_tts + platforms: + ios: + pluginClass: FlutterTtsPlugin + dartPluginClass: FlutterTtsIos diff --git a/packages/flutter_tts_macos/.gitignore b/packages/flutter_tts_macos/.gitignore new file mode 100644 index 00000000..53e92cc4 --- /dev/null +++ b/packages/flutter_tts_macos/.gitignore @@ -0,0 +1,3 @@ +.packages +.flutter-plugins +pubspec.lock diff --git a/packages/flutter_tts_macos/README.md b/packages/flutter_tts_macos/README.md new file mode 100644 index 00000000..8a062dbc --- /dev/null +++ b/packages/flutter_tts_macos/README.md @@ -0,0 +1,14 @@ +# flutter_tts_macos + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] + +The macos implementation of `flutter_tts`. + +## Usage + +This package is [endorsed][endorsed_link], which means you can simply use `flutter_tts` +normally. This package will be automatically included in your app when you do. + +[endorsed_link]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis diff --git a/packages/flutter_tts_macos/analysis_options.yaml b/packages/flutter_tts_macos/analysis_options.yaml new file mode 100644 index 00000000..4c56a11a --- /dev/null +++ b/packages/flutter_tts_macos/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:very_good_analysis/analysis_options.yaml +analyzer: + errors: + lines_longer_than_80_chars: ignore diff --git a/packages/flutter_tts_macos/lib/flutter_tts_macos.dart b/packages/flutter_tts_macos/lib/flutter_tts_macos.dart new file mode 100644 index 00000000..7ba82da4 --- /dev/null +++ b/packages/flutter_tts_macos/lib/flutter_tts_macos.dart @@ -0,0 +1,59 @@ +import 'package:flutter_tts_platform_interface/flutter_tts_platform_interface.dart'; + +/// The macOS implementation of [FlutterTtsPlatform]. +class FlutterTtsMacos extends FlutterTtsMethodChannel { + /// Registers this class as the default instance of [FlutterTtsPlatform] + static void registerWith() { + FlutterTtsPlatform.instance = FlutterTtsMacos(); + } + + final _macosHostApi = MacosTtsHostApi(); + + /// [Future] which sets synthesize to file's future to return + /// on completion of the synthesize + Future> awaitSynthCompletion({ + required bool awaitCompletion, + }) async { + try { + return ResultDart.success( + await _macosHostApi.awaitSynthCompletion(awaitCompletion), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for + /// getting the speech rate valid range + Future> getSpeechRateValidRange() async { + try { + return ResultDart.success(await _macosHostApi.getSpeechRateValidRange()); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for + /// setting the language + /// Macos 10.15 or below does not support Voice selection, + /// use Language selection instead + Future> setLanguange(String language) async { + try { + return ResultDart.success(await _macosHostApi.setLanguange(language)); + } on Exception catch (e) { + return ResultDart.error(e); + } + } + + /// [Future] which invokes the platform specific method for + /// checking if the language is available, see also [setLanguange] + Future> isLanguageAvailable(String language) async { + try { + return ResultDart.success( + await _macosHostApi.isLanguageAvailable(language), + ); + } on Exception catch (e) { + return ResultDart.error(e); + } + } +} diff --git a/macos/Classes/FlutterTtsPlugin.swift b/packages/flutter_tts_macos/macos/Classes/FlutterTtsPlugin.swift similarity index 100% rename from macos/Classes/FlutterTtsPlugin.swift rename to packages/flutter_tts_macos/macos/Classes/FlutterTtsPlugin.swift diff --git a/macos/Classes/message.g.swift b/packages/flutter_tts_macos/macos/Classes/message.g.swift similarity index 100% rename from macos/Classes/message.g.swift rename to packages/flutter_tts_macos/macos/Classes/message.g.swift diff --git a/macos/flutter_tts.podspec b/packages/flutter_tts_macos/macos/flutter_tts_macos.podspec similarity index 94% rename from macos/flutter_tts.podspec rename to packages/flutter_tts_macos/macos/flutter_tts_macos.podspec index 2838eb07..a749a764 100644 --- a/macos/flutter_tts.podspec +++ b/packages/flutter_tts_macos/macos/flutter_tts_macos.podspec @@ -2,7 +2,7 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| - s.name = 'flutter_tts' + s.name = 'flutter_tts_macos' s.version = '0.0.1' s.summary = 'macOS implementation of the flutter_tts plugin.' s.description = <<-DESC diff --git a/packages/flutter_tts_macos/pubspec.yaml b/packages/flutter_tts_macos/pubspec.yaml new file mode 100644 index 00000000..5349b1f5 --- /dev/null +++ b/packages/flutter_tts_macos/pubspec.yaml @@ -0,0 +1,28 @@ +name: flutter_tts_macos +description: A flutter plugin for Text to Speech. This plugin is supported on iOS, macOS, Android, Web, & Windows. +version: 5.0.0 +homepage: https://github.com/dlutton/flutter_tts +resolution: workspace + +environment: + sdk: ^3.9.0 + +dependencies: + flutter: + sdk: flutter + flutter_tts_platform_interface: ^5.0.0 + multiple_result: ^5.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + lints: ^6.0.0 + very_good_analysis: ^10.0.0 + +flutter: + plugin: + implements: flutter_tts + platforms: + macos: + pluginClass: FlutterTtsPlugin + dartPluginClass: FlutterTtsMacos diff --git a/packages/flutter_tts_platform_interface/README.md b/packages/flutter_tts_platform_interface/README.md new file mode 100644 index 00000000..969b814a --- /dev/null +++ b/packages/flutter_tts_platform_interface/README.md @@ -0,0 +1,14 @@ +# flutter_tts_platform_interface + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] + +A common platform interface for the `flutter_tts` plugin. + +This interface allows platform-specific implementations of the `flutter_tts` plugin, as well as the plugin itself, to ensure they are supporting the same interface. + +# Usage + +To implement a new platform-specific implementation of `flutter_tts`, extend `FlutterTtsPlatform` with an implementation that performs the platform-specific behavior. + +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/flutter_tts_platform_interface/analysis_options.yaml b/packages/flutter_tts_platform_interface/analysis_options.yaml new file mode 100644 index 00000000..3b4a5ed8 --- /dev/null +++ b/packages/flutter_tts_platform_interface/analysis_options.yaml @@ -0,0 +1,7 @@ +include: package:very_good_analysis/analysis_options.yaml +analyzer: + errors: + lines_longer_than_80_chars: ignore + exclude: + - "**/*.g.dart" # 忽略所有 .g.dart + - "**/*.freezed.dart" diff --git a/packages/flutter_tts_platform_interface/lib/flutter_tts_platform_interface.dart b/packages/flutter_tts_platform_interface/lib/flutter_tts_platform_interface.dart new file mode 100644 index 00000000..9b63c982 --- /dev/null +++ b/packages/flutter_tts_platform_interface/lib/flutter_tts_platform_interface.dart @@ -0,0 +1,146 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_tts_platform_interface/src/flutter_tts_method_channel.dart'; +import 'package:flutter_tts_platform_interface/src/messages.g.dart'; +import 'package:multiple_result/multiple_result.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +export 'package:flutter_tts_platform_interface/src/flutter_tts_method_channel.dart'; +export 'package:flutter_tts_platform_interface/src/flutter_tts_mixin.dart'; +export 'package:flutter_tts_platform_interface/src/messages.g.dart'; + +/// The result type for Flutter TTS platform methods. +typedef ResultDart = Result; + +/// The success type for Flutter TTS platform methods. +typedef SuccessDart = Success; + +/// The error type for Flutter TTS platform methods. +typedef FailureDart = Error; + +/// The abstract class which the platform implementations must extend. +abstract class FlutterTtsPlatform extends PlatformInterface { + /// constructor + FlutterTtsPlatform() : super(token: _token); + static const _token = Object(); + + static FlutterTtsPlatform _instance = FlutterTtsMethodChannel(); + + /// The default instance of [FlutterTtsPlatform ] to use. + /// + /// Defaults to [FlutterTtsMethodChannel]. + static FlutterTtsPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [FlutterTtsPlatform ] when + /// they register themselves. + static set instance(FlutterTtsPlatform instance) { + PlatformInterface.verifyToken(instance, FlutterTtsPlatform._token); + _instance = instance; + } + + /// Callbacks for Flutter TTS events. + /// NOTE: Not all platforms support all callbacks. + /// on speak start + VoidCallback? onSpeakStart; + + /// on speak complete + VoidCallback? onSpeakComplete; + + /// on speak pause + VoidCallback? onSpeakPause; + + /// on speak resume + VoidCallback? onSpeakResume; + + /// on speak cancel + VoidCallback? onSpeakCancel; + + /// on speak error + ValueChanged? onSpeakError; + + /// on speak progress + ValueChanged? onSpeakProgress; + + /// on synth start + /// NOTE: Not all platforms support this callback. + VoidCallback? onSynthStart; + + /// on synth complete + /// NOTE: Not all platforms support this callback. + VoidCallback? onSynthComplete; + + /// on synth error + /// NOTE: Not all platforms support this callback. + ValueChanged? onSynthError; + + /// on synth progress + /// NOTE: Not all platforms support this callback. + ValueChanged? onSynthProgress; + + /// [Future] which sets speak's future to return on completion of the utterance + Future> awaitSpeakCompletion({ + required bool awaitCompletion, + }) { + throw UnimplementedError( + 'awaitSpeakCompletion() has not been implemented.', + ); + } + + /// [Future] which invokes the platform specific method for speaking + Future> speak(String text, {bool focus = false}) { + throw UnimplementedError('speak() has not been implemented.'); + } + + /// [Future] which invokes the platform specific method for pause + Future> pause() { + throw UnimplementedError('pause() has not been implemented.'); + } + + /// [Future] which invokes the platform specific method for stop + Future> stop() { + throw UnimplementedError('stop() has not been implemented.'); + } + + /// [Future] which invokes the platform specific method for setSpeechRate + /// Allowed values are in the range from 0.0 (slowest) to 1.0 (fastest) + Future> setSpeechRate(double rate) { + throw UnimplementedError('setSpeechRate() has not been implemented.'); + } + + /// [Future] which invokes the platform specific method for setVolume + /// Allowed values are in the range from 0.0 (silent) to 1.0 (loudest) + Future> setVolume(double volume) { + throw UnimplementedError('setVolume() has not been implemented.'); + } + + /// [Future] which invokes the platform specific method for setPitch + /// 1.0 is default and ranges from .5 to 2.0 + Future> setPitch(double pitch) { + throw UnimplementedError('setPitch() has not been implemented.'); + } + + /// [Future] which invokes the platform specific method for getLanguages + /// Returns a `List` of `Strings` containing the supported languages + Future>> getLanguages() { + throw UnimplementedError('getLanguages() has not been implemented.'); + } + + /// [Future] which invokes the platform specific method for getVoices + /// Returns a `List` of `Maps` containing a voice name and locale + /// For iOS specifically, it also includes quality, gender, and identifier + /// ***Android, iOS, and macOS supported only*** + Future>> getVoices() { + throw UnimplementedError('getVoices() has not been implemented.'); + } + + /// [Future] which invokes the platform specific method for setVoice + Future> setVoice(Voice voice) { + throw UnimplementedError('setVoice() has not been implemented.'); + } + + /// [Future] which resets the platform voice to the default + Future> clearVoice() { + throw UnimplementedError('clearVoice() has not been implemented.'); + } +} diff --git a/packages/flutter_tts_platform_interface/lib/src/flutter_tts_method_channel.dart b/packages/flutter_tts_platform_interface/lib/src/flutter_tts_method_channel.dart new file mode 100644 index 00000000..1f7099ac --- /dev/null +++ b/packages/flutter_tts_platform_interface/lib/src/flutter_tts_method_channel.dart @@ -0,0 +1,5 @@ +import 'package:flutter_tts_platform_interface/flutter_tts_platform_interface.dart'; + +/// The method channel implementation of [FlutterTtsPlatform]. +class FlutterTtsMethodChannel extends FlutterTtsPlatform + with FlutterTtsPigeonMixin {} diff --git a/lib/src/flutter_tts_method_channel.dart b/packages/flutter_tts_platform_interface/lib/src/flutter_tts_mixin.dart similarity index 78% rename from lib/src/flutter_tts_method_channel.dart rename to packages/flutter_tts_platform_interface/lib/src/flutter_tts_mixin.dart index 90a1f665..e66a6630 100644 --- a/lib/src/flutter_tts_method_channel.dart +++ b/packages/flutter_tts_platform_interface/lib/src/flutter_tts_mixin.dart @@ -1,21 +1,33 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_tts/src/flutter_tts_platform_interface.dart'; -import 'package:flutter_tts/src/messages.g.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_tts_platform_interface/flutter_tts_platform_interface.dart'; import 'package:multiple_result/multiple_result.dart'; -class FlutterTtsMethodChannel extends FlutterTtsPlatform - implements TtsFlutterApi { - @protected +/// In flutter federated plugin, if a implementation class, +/// e.g. FlutterTtsAndroid, does not extend [FlutterTtsPlatform], +/// FlutterTtsAndroid.registerWith will not be called +/// to share the same some common implementation, mixin is used here. +/// The pigeon implementation of [FlutterTtsPlatform]. +mixin FlutterTtsPigeonMixin on FlutterTtsPlatform implements TtsFlutterApi { + /// The pigeon host API for Flutter TTS. final TtsHostApi hostApi = TtsHostApi(); - FlutterTtsMethodChannel() { + bool _isTtsCallbackSetUp = false; + + /// Ensures that the TTS callback is set up. + @protected + void ensureSetupTtsCallback() { + if (_isTtsCallbackSetUp) { + return; + } + TtsFlutterApi.setUp(this); + _isTtsCallbackSetUp = true; } @override - Future> awaitSpeakCompletion( - bool awaitCompletion, - ) async { + Future> awaitSpeakCompletion({ + required bool awaitCompletion, + }) async { try { return Result.success( await hostApi.awaitSpeakCompletion(awaitCompletion), @@ -100,6 +112,7 @@ class FlutterTtsMethodChannel extends FlutterTtsPlatform @override Future> speak(String text, {bool focus = false}) async { try { + ensureSetupTtsCallback(); return Result.success(await hostApi.speak(text, focus)); } on Exception catch (e) { return Result.error(e); diff --git a/lib/src/messages.g.dart b/packages/flutter_tts_platform_interface/lib/src/messages.g.dart similarity index 100% rename from lib/src/messages.g.dart rename to packages/flutter_tts_platform_interface/lib/src/messages.g.dart diff --git a/packages/flutter_tts_platform_interface/pubspec.yaml b/packages/flutter_tts_platform_interface/pubspec.yaml new file mode 100644 index 00000000..ab89f9bf --- /dev/null +++ b/packages/flutter_tts_platform_interface/pubspec.yaml @@ -0,0 +1,20 @@ +name: flutter_tts_platform_interface +description: A flutter plugin for Text to Speech. This plugin is supported on iOS, macOS, Android, Web, & Windows. +version: 5.0.0 +homepage: https://github.com/dlutton/flutter_tts +resolution: workspace + +environment: + sdk: ^3.9.0 + +dependencies: + flutter: + sdk: flutter + multiple_result: ^5.2.0 + plugin_platform_interface: ^2.1.8 + +dev_dependencies: + flutter_test: + sdk: flutter + lints: ^6.0.0 + very_good_analysis: ^10.0.0 diff --git a/packages/flutter_tts_web/.gitignore b/packages/flutter_tts_web/.gitignore new file mode 100644 index 00000000..53e92cc4 --- /dev/null +++ b/packages/flutter_tts_web/.gitignore @@ -0,0 +1,3 @@ +.packages +.flutter-plugins +pubspec.lock diff --git a/packages/flutter_tts_web/README.md b/packages/flutter_tts_web/README.md new file mode 100644 index 00000000..bc9061dc --- /dev/null +++ b/packages/flutter_tts_web/README.md @@ -0,0 +1,14 @@ +# flutter_tts_web + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] + +The web implementation of `flutter_tts`. + +## Usage + +This package is [endorsed][endorsed_link], which means you can simply use `flutter_tts` +normally. This package will be automatically included in your app when you do. + +[endorsed_link]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis diff --git a/packages/flutter_tts_web/analysis_options.yaml b/packages/flutter_tts_web/analysis_options.yaml new file mode 100644 index 00000000..46a43712 --- /dev/null +++ b/packages/flutter_tts_web/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:very_good_analysis/analysis_options.yaml + +analyzer: + errors: + lines_longer_than_80_chars: ignore + public_member_api_docs: ignore diff --git a/lib/src/flutter_tts_web.dart b/packages/flutter_tts_web/lib/flutter_tts_web.dart similarity index 51% rename from lib/src/flutter_tts_web.dart rename to packages/flutter_tts_web/lib/flutter_tts_web.dart index b14ab046..20dcaaf9 100644 --- a/lib/src/flutter_tts_web.dart +++ b/packages/flutter_tts_web/lib/flutter_tts_web.dart @@ -2,49 +2,57 @@ import 'dart:async'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; -import 'package:flutter_tts/src/flutter_tts_platform_interface.dart'; -import 'package:flutter_tts/src/flutter_tts_web_interop_types.dart'; -import 'package:flutter_tts/src/messages.g.dart'; +import 'package:flutter_tts_platform_interface/flutter_tts_platform_interface.dart'; +import 'package:flutter_tts_web/flutter_tts_web_interop_types.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -export 'package:flutter_tts/src/flutter_tts_web_interop_types.dart'; - -enum TtsState { playing, stopped, paused, continued } +enum _TtsState { playing, stopped, paused, continued } +/// [FlutterTtsWeb] class for the web platform. class FlutterTtsWeb extends FlutterTtsPlatform { + /// Constructor for [FlutterTtsWeb]. + FlutterTtsWeb() { + try { + _utterance = SpeechSynthesisUtterance(); + _listeners(); + supported = true; + } on Exception catch (e) { + /// print is safe to use on flutter Web + /// ignore: avoid_print + print('Initialization of TTS failed. Functions are disabled. Error: $e'); + } + } + + /// Registers the plugin with the Flutter engine. static void registerWith(Registrar registrar) { FlutterTtsPlatform.instance = FlutterTtsWeb(); } - bool isAwaitSpeakCompletion = false; + /// Returns whether the TTS engine is currently playing. + bool get isPlaying => _ttsState == _TtsState.playing; - TtsState ttsState = TtsState.stopped; + /// Returns whether the TTS engine is currently stopped. + bool get isStopped => _ttsState == _TtsState.stopped; - Completer? _speechCompleter; + /// Returns whether the TTS engine is currently paused. + bool get isPaused => _ttsState == _TtsState.paused; - bool get isPlaying => ttsState == TtsState.playing; + /// Returns whether the TTS engine is currently continued. + bool get isContinued => _ttsState == _TtsState.continued; - bool get isStopped => ttsState == TtsState.stopped; + /// Returns whether the TTS engine is supported on the current platform. + bool supported = false; - bool get isPaused => ttsState == TtsState.paused; + bool _isAwaitSpeakCompletion = false; - bool get isContinued => ttsState == TtsState.continued; + _TtsState _ttsState = _TtsState.stopped; - late final SpeechSynthesisUtterance utterance; - List voices = []; - List languages = []; - Timer? t; - bool supported = false; + Completer? _speechCompleter; - FlutterTtsWeb() { - try { - utterance = SpeechSynthesisUtterance(); - _listeners(); - supported = true; - } catch (e) { - print('Initialization of TTS failed. Functions are disabled. Error: $e'); - } - } + late final SpeechSynthesisUtterance _utterance; + List _voices = []; + List _languages = []; + Timer? _timer; @override Future>> getVoices() async { @@ -68,13 +76,13 @@ class FlutterTtsWeb extends FlutterTtsPlatform { @override Future> setPitch(double pitch) async { - _setPitch(pitch); + _utterance.pitch = pitch; return ResultDart.success(TtsResult(success: true)); } @override Future> setSpeechRate(double rate) async { - _setRate(rate); + _utterance.rate = rate; return ResultDart.success(TtsResult(success: true)); } @@ -86,14 +94,14 @@ class FlutterTtsWeb extends FlutterTtsPlatform { @override Future> setVolume(double volume) async { - _setVolume(volume); + _utterance.volume = volume; return ResultDart.success(TtsResult(success: true)); } @override Future> speak(String text, {bool focus = false}) async { _speak(text); - if (isAwaitSpeakCompletion) { + if (_isAwaitSpeakCompletion) { _speechCompleter = Completer(); return ResultDart.success(await _speechCompleter!.future); } @@ -107,11 +115,12 @@ class FlutterTtsWeb extends FlutterTtsPlatform { return ResultDart.success(TtsResult(success: true)); } + /// Await the completion of the current speech. @override - Future> awaitSpeakCompletion( - bool awaitCompletion, - ) async { - isAwaitSpeakCompletion = awaitCompletion; + Future> awaitSpeakCompletion({ + required bool awaitCompletion, + }) async { + _isAwaitSpeakCompletion = awaitCompletion; return ResultDart.success(TtsResult(success: true)); } @@ -124,20 +133,22 @@ class FlutterTtsWeb extends FlutterTtsPlatform { } } + /// Check if a language is available on the current platform. Future isLanguageAvailable(String lang) async { return _isLanguageAvailable(lang); } void _listeners() { - utterance.onStart = (JSAny e) { - ttsState = TtsState.playing; + _utterance.onStart = (JSAny e) { + _ttsState = _TtsState.playing; onSpeakStart?.call(); - var bLocal = (utterance.voice?.isLocalService ?? false); + final bLocal = _utterance.voice?.isLocalService ?? false; if (!bLocal) { - t = Timer.periodic(Duration(seconds: 14), (t) { - if (ttsState == TtsState.playing) { - synth.pause(); - synth.resume(); + _timer = Timer.periodic(const Duration(seconds: 14), (t) { + if (_ttsState == _TtsState.playing) { + synth + ..pause() + ..resume(); } else { t.cancel(); } @@ -148,48 +159,59 @@ class FlutterTtsWeb extends FlutterTtsPlatform { // ttsState = TtsState.playing; // channel.invokeMethod("speak.onStart", null); // }); - utterance.onEnd = (JSAny e) { - ttsState = TtsState.stopped; + _utterance.onEnd = (JSAny e) { + _ttsState = _TtsState.stopped; if (_speechCompleter != null) { _speechCompleter?.complete(TtsResult(success: true)); _speechCompleter = null; } - t?.cancel(); + _timer?.cancel(); onSpeakComplete?.call(); }.toJS; - utterance.onPause = (JSAny e) { - ttsState = TtsState.paused; + _utterance.onPause = (JSAny e) { + _ttsState = _TtsState.paused; onSpeakPause?.call(); }.toJS; - utterance.onResume = (JSAny e) { - ttsState = TtsState.continued; + _utterance.onResume = (JSAny e) { + _ttsState = _TtsState.continued; onSpeakResume?.call(); }.toJS; - utterance.onError = (JSObject event) { - ttsState = TtsState.stopped; + _utterance.onError = (JSObject event) { + _ttsState = _TtsState.stopped; if (_speechCompleter != null) { _speechCompleter = null; } - t?.cancel(); + _timer?.cancel(); + + /// print is safe to use on flutter Web + /// ignore: avoid_print print(event); // Log the entire event object to get more details - onSpeakError?.call(event["error"].toString()); + onSpeakError?.call(event['error'].toString()); }.toJS; - utterance.onBoundary = (JSObject event) { - int charIndex = event['charIndex'] as int; - String name = event['name'] as String; + _utterance.onBoundary = (JSObject event) { + /// not sure about the impl, ignore for now + /// ignore: cast_nullable_to_non_nullable,invalid_runtime_check_with_js_interop_types + final charIndex = event['charIndex'] as int; + + /// not sure about the impl, ignore for now + /// ignore: cast_nullable_to_non_nullable,invalid_runtime_check_with_js_interop_types + final name = event['name'] as String; if (name == 'sentence') return; - String text = utterance['text'] as String; - int endIndex = charIndex; + + /// not sure about the impl, ignore for now + /// ignore: cast_nullable_to_non_nullable,invalid_runtime_check_with_js_interop_types + final text = _utterance['text'] as String; + var endIndex = charIndex; while (endIndex < text.length && !RegExp(r'[\s,.!?]').hasMatch(text[endIndex])) { endIndex++; } - String word = text.substring(charIndex, endIndex); - TtsProgress progress = TtsProgress( + final word = text.substring(charIndex, endIndex); + final progress = TtsProgress( text: text, start: charIndex, end: endIndex, @@ -201,47 +223,43 @@ class FlutterTtsWeb extends FlutterTtsPlatform { void _speak(String? text) { if (text == null || text.isEmpty) return; - if (ttsState == TtsState.stopped || ttsState == TtsState.paused) { - utterance.text = text; - if (ttsState == TtsState.paused) { + if (_ttsState == _TtsState.stopped || _ttsState == _TtsState.paused) { + _utterance.text = text; + if (_ttsState == _TtsState.paused) { synth.resume(); } else { - synth.speak(utterance); + synth.speak(_utterance); } } } void _stop() { - if (ttsState != TtsState.stopped) { + if (_ttsState != _TtsState.stopped) { synth.cancel(); } } void _pause() { - if (ttsState == TtsState.playing || ttsState == TtsState.continued) { + if (_ttsState == _TtsState.playing || _ttsState == _TtsState.continued) { synth.pause(); } } - void _setRate(double rate) => utterance.rate = rate; - void _setVolume(double volume) => utterance.volume = volume; - void _setPitch(double pitch) => utterance.pitch = pitch; - void _setVoice(Voice voice) { - var tmpVoices = synth.getVoices().toDart; - var targetList = tmpVoices.where((e) { + final tmpVoices = synth.getVoices().toDart; + final targetList = tmpVoices.where((e) { return voice.name == e.name && voice.locale == e.lang; }); if (targetList.isNotEmpty) { - utterance.voice = targetList.first; + _utterance.voice = targetList.first; } } bool _isLanguageAvailable(String? language) { - if (voices.isEmpty) _updateVoices(); - if (languages.isEmpty) _updateLanguages(); - for (var lang in languages) { + if (_voices.isEmpty) _updateVoices(); + if (_languages.isEmpty) _updateLanguages(); + for (var lang in _languages) { if (!language!.contains('-')) { lang = lang.split('-').first; } @@ -251,28 +269,28 @@ class FlutterTtsWeb extends FlutterTtsPlatform { } List? _getLanguages() { - if (voices.isEmpty) _updateVoices(); - if (languages.isEmpty) _updateLanguages(); - return languages; + if (_voices.isEmpty) _updateVoices(); + if (_languages.isEmpty) _updateLanguages(); + return _languages; } Future> _getVoices() async { _updateVoices(); - return voices + return _voices .map((voice) => Voice(name: voice.name, locale: voice.lang)) .toList(); } void _updateVoices() { - voices = synth.getVoices().toDart; + _voices = synth.getVoices().toDart; } void _updateLanguages() { - var langs = {}; - for (var v in voices) { + final langs = {}; + for (final v in _voices) { langs.add(v.lang); } - languages = langs.toList(); + _languages = langs.toList(); } } diff --git a/lib/src/flutter_tts_web_interop_types.dart b/packages/flutter_tts_web/lib/flutter_tts_web_interop_types.dart similarity index 75% rename from lib/src/flutter_tts_web_interop_types.dart rename to packages/flutter_tts_web/lib/flutter_tts_web_interop_types.dart index 431a7a10..b6707cf4 100644 --- a/lib/src/flutter_tts_web_interop_types.dart +++ b/packages/flutter_tts_web/lib/flutter_tts_web_interop_types.dart @@ -35,21 +35,33 @@ extension type SpeechSynthesisUtterance._(JSObject _) implements JSObject { // Event listeners @JS('onstart') + /// do not need a getter + /// ignore: avoid_setters_without_getters external set onStart(JSFunction listener); @JS('onend') + /// do not need a getter + /// ignore: avoid_setters_without_getters external set onEnd(JSFunction listener); @JS('onpause') + /// do not need a getter + /// ignore: avoid_setters_without_getters external set onPause(JSFunction listener); @JS('onresume') + /// do not need a getter + /// ignore: avoid_setters_without_getters external set onResume(JSFunction listener); @JS('onerror') + /// do not need a getter + /// ignore: avoid_setters_without_getters external set onError(JSFunction listener); @JS('onboundary') + /// do not need a getter + /// ignore: avoid_setters_without_getters external set onBoundary(JSFunction listener); } diff --git a/packages/flutter_tts_web/pubspec.yaml b/packages/flutter_tts_web/pubspec.yaml new file mode 100644 index 00000000..1cefd68d --- /dev/null +++ b/packages/flutter_tts_web/pubspec.yaml @@ -0,0 +1,30 @@ +name: flutter_tts_web +description: A flutter plugin for Text to Speech. This plugin is supported on iOS, macOS, Android, Web, & Windows. +version: 5.0.0 +homepage: https://github.com/dlutton/flutter_tts +resolution: workspace + +environment: + sdk: ^3.9.0 + +dependencies: + flutter: + sdk: flutter + flutter_tts_platform_interface: ^5.0.0 + flutter_web_plugins: + sdk: flutter + multiple_result: ^5.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + lints: ^6.0.0 + very_good_analysis: ^10.0.0 + +flutter: + plugin: + implements: flutter_tts + platforms: + web: + pluginClass: FlutterTtsWeb + fileName: flutter_tts_web.dart diff --git a/packages/flutter_tts_windows/.gitignore b/packages/flutter_tts_windows/.gitignore new file mode 100644 index 00000000..9be145fd --- /dev/null +++ b/packages/flutter_tts_windows/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/flutter_tts_windows/.metadata b/packages/flutter_tts_windows/.metadata new file mode 100644 index 00000000..8c15ad72 --- /dev/null +++ b/packages/flutter_tts_windows/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + channel: stable + +project_type: plugin diff --git a/packages/flutter_tts_windows/README.md b/packages/flutter_tts_windows/README.md new file mode 100644 index 00000000..72688da2 --- /dev/null +++ b/packages/flutter_tts_windows/README.md @@ -0,0 +1,14 @@ +# flutter_tts_windows + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] + +The windows implementation of `flutter_tts`. + +## Usage + +This package is [endorsed][endorsed_link], which means you can simply use `flutter_tts` +normally. This package will be automatically included in your app when you do. + +[endorsed_link]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis diff --git a/packages/flutter_tts_windows/analysis_options.yaml b/packages/flutter_tts_windows/analysis_options.yaml new file mode 100644 index 00000000..9df80aa4 --- /dev/null +++ b/packages/flutter_tts_windows/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.yaml diff --git a/packages/flutter_tts_windows/lib/flutter_tts_windows.dart b/packages/flutter_tts_windows/lib/flutter_tts_windows.dart new file mode 100644 index 00000000..9c044509 --- /dev/null +++ b/packages/flutter_tts_windows/lib/flutter_tts_windows.dart @@ -0,0 +1,9 @@ +import 'package:flutter_tts_platform_interface/flutter_tts_platform_interface.dart'; + +/// The Windows implementation of [FlutterTtsPlatform]. +class FlutterTtsWindows extends FlutterTtsPlatform with FlutterTtsPigeonMixin { + /// Registers this class as the default instance of [FlutterTtsPlatform] + static void registerWith() { + FlutterTtsPlatform.instance = FlutterTtsWindows(); + } +} diff --git a/packages/flutter_tts_windows/pubspec.yaml b/packages/flutter_tts_windows/pubspec.yaml new file mode 100644 index 00000000..ca5ba848 --- /dev/null +++ b/packages/flutter_tts_windows/pubspec.yaml @@ -0,0 +1,27 @@ +name: flutter_tts_windows +description: A flutter plugin for Text to Speech. This plugin is supported on iOS, macOS, Android, Web, & Windows. +version: 5.0.0 +homepage: https://github.com/dlutton/flutter_tts +resolution: workspace + +environment: + sdk: ^3.9.0 + +dependencies: + flutter: + sdk: flutter + flutter_tts_platform_interface: ^5.0.0 + multiple_result: ^5.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + very_good_analysis: ^10.0.0 + +flutter: + plugin: + implements: flutter_tts + platforms: + windows: + pluginClass: FlutterTtsWindows + dartPluginClass: FlutterTtsWindows diff --git a/windows/.gitignore b/packages/flutter_tts_windows/windows/.gitignore similarity index 100% rename from windows/.gitignore rename to packages/flutter_tts_windows/windows/.gitignore diff --git a/windows/CMakeLists.txt b/packages/flutter_tts_windows/windows/CMakeLists.txt similarity index 96% rename from windows/CMakeLists.txt rename to packages/flutter_tts_windows/windows/CMakeLists.txt index 7070ecab..948d67a4 100644 --- a/windows/CMakeLists.txt +++ b/packages/flutter_tts_windows/windows/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.14) if(POLICY CMP0153) cmake_policy(SET CMP0153 NEW) endif() -set(PROJECT_NAME "flutter_tts") +set(PROJECT_NAME "flutter_tts_windows") project(${PROJECT_NAME} LANGUAGES CXX) ################ NuGet intall begin ################ @@ -20,7 +20,7 @@ execute_process( # This value is used when generating builds using this plugin, so it must # not be changed -set(PLUGIN_NAME "flutter_tts_plugin") +set(PLUGIN_NAME "${PROJECT_NAME}_plugin") add_library(${PLUGIN_NAME} SHARED "flutter_tts_plugin.cpp" diff --git a/windows/flutter_tts_plugin.cpp b/packages/flutter_tts_windows/windows/flutter_tts_plugin.cpp similarity index 99% rename from windows/flutter_tts_plugin.cpp rename to packages/flutter_tts_windows/windows/flutter_tts_plugin.cpp index f84d95bd..15cb6e70 100644 --- a/windows/flutter_tts_plugin.cpp +++ b/packages/flutter_tts_windows/windows/flutter_tts_plugin.cpp @@ -1,4 +1,4 @@ -#include "include/flutter_tts/flutter_tts_plugin.h" +#include "include/flutter_tts_windows/flutter_tts_windows.h" #include // This must be included before many other Windows headers. @@ -593,7 +593,7 @@ void FlutterTtsPlugin::getLanguages(flutter::EncodableList& languages) { } // namespace -void FlutterTtsPluginRegisterWithRegistrar( +void FlutterTtsWindowsRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar) { FlutterTtsPlugin::RegisterWithRegistrar( flutter::PluginRegistrarManager::GetInstance() diff --git a/windows/include/flutter_tts/flutter_tts_plugin.h b/packages/flutter_tts_windows/windows/include/flutter_tts_windows/flutter_tts_windows.h similarity index 87% rename from windows/include/flutter_tts/flutter_tts_plugin.h rename to packages/flutter_tts_windows/windows/include/flutter_tts_windows/flutter_tts_windows.h index 8feadf91..887d9963 100644 --- a/windows/include/flutter_tts/flutter_tts_plugin.h +++ b/packages/flutter_tts_windows/windows/include/flutter_tts_windows/flutter_tts_windows.h @@ -13,7 +13,7 @@ extern "C" { #endif -FLUTTER_PLUGIN_EXPORT void FlutterTtsPluginRegisterWithRegistrar( +FLUTTER_PLUGIN_EXPORT void FlutterTtsWindowsRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar); #if defined(__cplusplus) diff --git a/windows/messages.g.cpp b/packages/flutter_tts_windows/windows/messages.g.cpp similarity index 99% rename from windows/messages.g.cpp rename to packages/flutter_tts_windows/windows/messages.g.cpp index 6cee4294..385a9ff6 100644 --- a/windows/messages.g.cpp +++ b/packages/flutter_tts_windows/windows/messages.g.cpp @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// Autogenerated from Pigeon (v26.1.0), do not edit directly. // See also: https://pub.dev/packages/pigeon #undef _HAS_EXCEPTIONS diff --git a/windows/messages.g.h b/packages/flutter_tts_windows/windows/messages.g.h similarity index 99% rename from windows/messages.g.h rename to packages/flutter_tts_windows/windows/messages.g.h index 209c4122..eb8f28d8 100644 --- a/windows/messages.g.h +++ b/packages/flutter_tts_windows/windows/messages.g.h @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.1.0), do not edit directly. +// Autogenerated from Pigeon (v26.1.0), do not edit directly. // See also: https://pub.dev/packages/pigeon #ifndef PIGEON_MESSAGES_G_H_ diff --git a/pigeons/messages.dart b/pigeons/messages.dart index b9d4507b..735e6f13 100644 --- a/pigeons/messages.dart +++ b/pigeons/messages.dart @@ -2,16 +2,16 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( - dartOut: 'lib/src/messages.g.dart', + dartOut: 'packages/flutter_tts_platform_interface/lib/src/messages.g.dart', dartOptions: DartOptions(), - cppHeaderOut: 'windows/messages.g.h', - cppSourceOut: 'windows/messages.g.cpp', + cppHeaderOut: 'packages/flutter_tts_windows/windows/messages.g.h', + cppSourceOut: 'packages/flutter_tts_windows/windows/messages.g.cpp', cppOptions: CppOptions(namespace: 'flutter_tts'), dartPackageName: 'flutter_tts', kotlinOut: - 'android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt', + 'packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt', kotlinOptions: KotlinOptions(package: "com.tundralabs.fluttertts"), - swiftOut: "macos/Classes/message.g.swift", + swiftOut: "packages/flutter_tts_macos/macos/Classes/message.g.swift", ), ) enum FlutterTtsErrorCode { diff --git a/pubspec.yaml b/pubspec.yaml index fed8916a..95b43f3d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,46 +1,20 @@ -name: flutter_tts -description: A flutter plugin for Text to Speech. This plugin is supported on iOS, macOS, Android, Web, & Windows. -version: 5.0.0 -homepage: https://github.com/dlutton/flutter_tts - +name: flutter_tts_workspace environment: sdk: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" - -dependencies: - flutter: - sdk: flutter - flutter_web_plugins: - sdk: flutter - multiple_result: ^5.2.0 - plugin_platform_interface: ^2.1.8 - dev_dependencies: - lints: ^6.0.0 + melos: ^7.3.0 path: ^1.9.1 - pigeon: ^26.0.5 - -flutter: - plugin: - platforms: - android: - package: com.tundralabs.fluttertts - pluginClass: FlutterTtsPlugin - dartPluginClass: FlutterTtsAndroid - fileName: src/flutter_tts_android.dart - ios: - pluginClass: FlutterTtsPlugin - dartPluginClass: FlutterTtsIos - fileName: src/flutter_tts_ios.dart - macos: - pluginClass: FlutterTtsPlugin - dartPluginClass: FlutterTtsMacos - fileName: src/flutter_tts_macos.dart - windows: - pluginClass: FlutterTtsPlugin - supportedVariants: - - uwp - - win32 - web: - pluginClass: FlutterTtsWeb - fileName: src/flutter_tts_web.dart + pigeon: ^26.1.0 +workspace: + - apps/example + - packages/flutter_tts + - packages/flutter_tts_android + - packages/flutter_tts_macos + - packages/flutter_tts_ios + - packages/flutter_tts_platform_interface + - packages/flutter_tts_web + - packages/flutter_tts_windows +melos: + scripts: + gen_pigeon: + run: dart run tools/generate_pigeons.dart diff --git a/tools/generate_pigeons.dart b/tools/generate_pigeons.dart index 5a306a7d..78def4ff 100644 --- a/tools/generate_pigeons.dart +++ b/tools/generate_pigeons.dart @@ -27,27 +27,27 @@ void main() async { } // 确保iOS目录存在 - final iosDir = Directory(p.join(rootDir, 'ios', 'Classes')); + final iosDir = Directory( + p.join(rootDir, 'packages', 'flutter_tts_ios', 'ios', 'Classes'), + ); if (!iosDir.existsSync()) { print('iOS dir not exists: ${iosDir.path}'); return; } - // 确保macOS目录存在 - final macosDir = Directory(p.join(rootDir, 'macos', 'Classes')); - if (!macosDir.existsSync()) { - print('macOS dir not exists: ${macosDir.path}'); - return; - } - // 为iOS单独生成Swift代码(由于pigeon可能不直接支持同时为多个平台生成Swift) // 这里通过手动复制生成的Swift文件到iOS目录 final macosSwiftFile = File( - p.join(rootDir, 'macos', 'Classes', 'message.g.swift'), - ); - await macosSwiftFile.copy( - p.join(rootDir, 'ios', 'Classes', 'message.g.swift'), + p.join( + rootDir, + 'packages', + 'flutter_tts_macos', + 'macos', + 'Classes', + 'message.g.swift', + ), ); + await macosSwiftFile.copy(p.join(iosDir.path, 'message.g.swift')); print('\n✅ done generating pigeon code!'); } catch (e) { From 04d212fcf7678c66f611daaac1c2ef7f09aaf3db Mon Sep 17 00:00:00 2001 From: fenghezhou Date: Mon, 17 Nov 2025 20:54:20 +0800 Subject: [PATCH 3/5] refactor: remove android mirror --- apps/example/android/gradle/wrapper/gradle-wrapper.properties | 4 ++-- apps/example/android/settings.gradle.kts | 2 -- .../android/gradle/wrapper/gradle-wrapper.properties | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/example/android/gradle/wrapper/gradle-wrapper.properties b/apps/example/android/gradle/wrapper/gradle-wrapper.properties index 26677571..cdebde04 100644 --- a/apps/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/apps/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,5 +3,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -# distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip -distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.13-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip +# distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.13-all.zip diff --git a/apps/example/android/settings.gradle.kts b/apps/example/android/settings.gradle.kts index f1ab4f39..75af8a40 100644 --- a/apps/example/android/settings.gradle.kts +++ b/apps/example/android/settings.gradle.kts @@ -11,8 +11,6 @@ pluginManagement { includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { - maven { url = uri("https://maven.aliyun.com/repository/google") } - maven { url = uri("https://maven.aliyun.com/repository/central") } google() mavenCentral() gradlePluginPortal() diff --git a/packages/flutter_tts_android/android/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_tts_android/android/gradle/wrapper/gradle-wrapper.properties index f14a403e..fc176a8e 100644 --- a/packages/flutter_tts_android/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/flutter_tts_android/android/gradle/wrapper/gradle-wrapper.properties @@ -3,5 +3,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -#distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip -distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.13-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip +# distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.13-all.zip From a2550e17c2fbd2373feedcc8daebdf6f5e1aedcb Mon Sep 17 00:00:00 2001 From: von Date: Wed, 19 Nov 2025 09:45:30 +0800 Subject: [PATCH 4/5] feat: example shows tts progress --- apps/example/lib/main.dart | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/apps/example/lib/main.dart b/apps/example/lib/main.dart index 74250c5b..849d4818 100644 --- a/apps/example/lib/main.dart +++ b/apps/example/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io' show Platform; +import 'dart:math' as math; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; @@ -52,6 +53,8 @@ class _MyAppState extends State { int? _inputLength; final _editingController = TextEditingController(); + TtsProgress? _speakingProgess; + TtsState ttsState = TtsState.stopped; bool get isPlaying => ttsState == TtsState.playing; @@ -91,6 +94,7 @@ class _MyAppState extends State { setState(() { print("Complete"); ttsState = TtsState.stopped; + _speakingProgess = null; }); }; @@ -98,6 +102,7 @@ class _MyAppState extends State { setState(() { print("Cancel"); ttsState = TtsState.stopped; + _speakingProgess = null; }); }; @@ -119,6 +124,13 @@ class _MyAppState extends State { print("error: $msg"); setState(() { ttsState = TtsState.stopped; + _speakingProgess = null; + }); + }; + + flutterTts.onSpeakProgress = (progress) { + setState(() { + _speakingProgess = progress; }); }; @@ -304,6 +316,26 @@ class _MyAppState extends State { scrollDirection: Axis.vertical, child: Column( children: [ + if (_speakingProgess case final progess?) + Text.rich( + TextSpan( + children: [ + if (progess.start > 0) + TextSpan( + text: progess.text.substring(0, progess.start), + ), + TextSpan( + text: progess.text.substring( + math.max(0, progess.start), + math.min(progess.text.length, progess.end), + ), + style: TextStyle(color: Colors.red), + ), + if (progess.end < progess.text.length - 1) + TextSpan(text: progess.text.substring(progess.end)), + ], + ), + ), _inputSection(), _btnSection(), _engineSection(), From fa04256de138fa955cd6af43077fbb1f2adda9f6 Mon Sep 17 00:00:00 2001 From: von Date: Wed, 19 Nov 2025 13:23:48 +0800 Subject: [PATCH 5/5] feat: support tts progress on Windows --- apps/example/lib/main.dart | 61 ++++-- apps/example/pubspec.yaml | 1 + .../com/tundralabs/fluttertts/messages.g.kt | 36 ++++ .../ios/Classes/message.g.swift | 30 +++ .../macos/Classes/message.g.swift | 30 +++ .../lib/src/messages.g.dart | 42 ++++ .../lib/flutter_tts_windows.dart | 16 ++ .../windows/flutter_tts_plugin.cpp | 179 ++++++++++++++++-- .../windows/messages.g.cpp | 64 +++++++ .../flutter_tts_windows/windows/messages.g.h | 30 +++ pigeons/messages.dart | 12 +- 11 files changed, 471 insertions(+), 30 deletions(-) diff --git a/apps/example/lib/main.dart b/apps/example/lib/main.dart index 849d4818..21a321e8 100644 --- a/apps/example/lib/main.dart +++ b/apps/example/lib/main.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_tts/flutter_tts.dart'; import 'package:flutter_tts_android/flutter_tts_android.dart'; +import 'package:flutter_tts_windows/flutter_tts_windows.dart'; extension on Voice { String get displayName { @@ -55,6 +56,8 @@ class _MyAppState extends State { TtsProgress? _speakingProgess; + bool _isWordBoundary = true; + TtsState ttsState = TtsState.stopped; bool get isPlaying => ttsState == TtsState.playing; @@ -316,26 +319,37 @@ class _MyAppState extends State { scrollDirection: Axis.vertical, child: Column( children: [ - if (_speakingProgess case final progess?) - Text.rich( - TextSpan( - children: [ - if (progess.start > 0) + if (_speakingProgess case final progess?) ...[ + Padding( + padding: EdgeInsets.only(top: 25.0, left: 25.0, right: 25.0), + child: Text( + progess.word, + style: TextStyle(color: Colors.red), + ), + ), + Padding( + padding: EdgeInsets.only(top: 25.0, left: 25.0, right: 25.0), + child: Text.rich( + TextSpan( + children: [ + if (progess.start > 0) + TextSpan( + text: progess.text.substring(0, progess.start), + ), TextSpan( - text: progess.text.substring(0, progess.start), - ), - TextSpan( - text: progess.text.substring( - math.max(0, progess.start), - math.min(progess.text.length, progess.end), + text: progess.text.substring( + math.max(0, progess.start), + math.min(progess.text.length, progess.end), + ), + style: TextStyle(color: Colors.red), ), - style: TextStyle(color: Colors.red), - ), - if (progess.end < progess.text.length - 1) - TextSpan(text: progess.text.substring(progess.end)), - ], + if (progess.end < progess.text.length - 1) + TextSpan(text: progess.text.substring(progess.end)), + ], + ), ), ), + ], _inputSection(), _btnSection(), _engineSection(), @@ -441,6 +455,21 @@ Cinq chiens chassent six chats. } }, ); + } else if (flutterTts case final FlutterTtsWindows winTts) { + return Row( + spacing: 8, + mainAxisSize: MainAxisSize.min, + children: [ + Text(_isWordBoundary ? "Word Boundary" : "Sentence Boundary"), + Switch( + value: _isWordBoundary, + onChanged: (value) async { + await winTts.setBoundaryType(isWordBoundary: value); + setState(() => _isWordBoundary = value); + }, + ), + ], + ); } else { return SizedBox(width: 0, height: 0); } diff --git a/apps/example/pubspec.yaml b/apps/example/pubspec.yaml index 1b473dc0..a50cf577 100644 --- a/apps/example/pubspec.yaml +++ b/apps/example/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: cupertino_icons: ^1.0.1+1 flutter_tts: ^5.0.0 flutter_tts_android: ^5.0.0 + flutter_tts_windows: ^5.0.0 dev_dependencies: flutter_test: diff --git a/packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt b/packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt index 228ef07b..edb2fdb7 100644 --- a/packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt +++ b/packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt @@ -1489,6 +1489,42 @@ interface MacosTtsHostApi { } } } +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface WinTtsHostApi { + fun setBoundaryType(isWordBoundary: Boolean, callback: (Result) -> Unit) + + companion object { + /** The codec used by WinTtsHostApi. */ + val codec: MessageCodec by lazy { + messagesPigeonCodec() + } + /** Sets up an instance of `WinTtsHostApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: WinTtsHostApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_tts.WinTtsHostApi.setBoundaryType$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val isWordBoundaryArg = args[0] as Boolean + api.setBoundaryType(isWordBoundaryArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} /** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ class TtsFlutterApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { companion object { diff --git a/packages/flutter_tts_ios/ios/Classes/message.g.swift b/packages/flutter_tts_ios/ios/Classes/message.g.swift index d2539406..d429d040 100644 --- a/packages/flutter_tts_ios/ios/Classes/message.g.swift +++ b/packages/flutter_tts_ios/ios/Classes/message.g.swift @@ -1342,6 +1342,36 @@ class MacosTtsHostApiSetup { } } } +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol WinTtsHostApi { + func setBoundaryType(isWordBoundary: Bool, completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class WinTtsHostApiSetup { + static var codec: FlutterStandardMessageCodec { MessagePigeonCodec.shared } + /// Sets up an instance of `WinTtsHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: WinTtsHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let setBoundaryTypeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.WinTtsHostApi.setBoundaryType\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setBoundaryTypeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let isWordBoundaryArg = args[0] as! Bool + api.setBoundaryType(isWordBoundary: isWordBoundaryArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setBoundaryTypeChannel.setMessageHandler(nil) + } + } +} /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. protocol TtsFlutterApiProtocol { func onSpeakStartCb(completion: @escaping (Result) -> Void) diff --git a/packages/flutter_tts_macos/macos/Classes/message.g.swift b/packages/flutter_tts_macos/macos/Classes/message.g.swift index d2539406..d429d040 100644 --- a/packages/flutter_tts_macos/macos/Classes/message.g.swift +++ b/packages/flutter_tts_macos/macos/Classes/message.g.swift @@ -1342,6 +1342,36 @@ class MacosTtsHostApiSetup { } } } +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol WinTtsHostApi { + func setBoundaryType(isWordBoundary: Bool, completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class WinTtsHostApiSetup { + static var codec: FlutterStandardMessageCodec { MessagePigeonCodec.shared } + /// Sets up an instance of `WinTtsHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: WinTtsHostApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let setBoundaryTypeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_tts.WinTtsHostApi.setBoundaryType\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setBoundaryTypeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let isWordBoundaryArg = args[0] as! Bool + api.setBoundaryType(isWordBoundary: isWordBoundaryArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setBoundaryTypeChannel.setMessageHandler(nil) + } + } +} /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. protocol TtsFlutterApiProtocol { func onSpeakStartCb(completion: @escaping (Result) -> Void) diff --git a/packages/flutter_tts_platform_interface/lib/src/messages.g.dart b/packages/flutter_tts_platform_interface/lib/src/messages.g.dart index 383a2dbc..678c2cc5 100644 --- a/packages/flutter_tts_platform_interface/lib/src/messages.g.dart +++ b/packages/flutter_tts_platform_interface/lib/src/messages.g.dart @@ -1680,6 +1680,48 @@ class MacosTtsHostApi { } } +class WinTtsHostApi { + /// Constructor for [WinTtsHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + WinTtsHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future setBoundaryType(bool isWordBoundary) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tts.WinTtsHostApi.setBoundaryType$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([isWordBoundary]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as TtsResult?)!; + } + } +} + abstract class TtsFlutterApi { static const MessageCodec pigeonChannelCodec = _PigeonCodec(); diff --git a/packages/flutter_tts_windows/lib/flutter_tts_windows.dart b/packages/flutter_tts_windows/lib/flutter_tts_windows.dart index 9c044509..c3faf60a 100644 --- a/packages/flutter_tts_windows/lib/flutter_tts_windows.dart +++ b/packages/flutter_tts_windows/lib/flutter_tts_windows.dart @@ -6,4 +6,20 @@ class FlutterTtsWindows extends FlutterTtsPlatform with FlutterTtsPigeonMixin { static void registerWith() { FlutterTtsPlatform.instance = FlutterTtsWindows(); } + + final _winHostApi = WinTtsHostApi(); + + /// Set the boundary type for the TTS engine. Word boundary by default + /// + /// [isWordBoundary] word boundary if true, else sentence boundary. + Future> setBoundaryType({ + required bool isWordBoundary, + }) async { + try { + final result = await _winHostApi.setBoundaryType(isWordBoundary); + return SuccessDart(result); + } on Exception catch (e) { + return ResultDart.error(e); + } + } } diff --git a/packages/flutter_tts_windows/windows/flutter_tts_plugin.cpp b/packages/flutter_tts_windows/windows/flutter_tts_plugin.cpp index 15cb6e70..a1c29abd 100644 --- a/packages/flutter_tts_windows/windows/flutter_tts_plugin.cpp +++ b/packages/flutter_tts_windows/windows/flutter_tts_plugin.cpp @@ -1,6 +1,6 @@ -#include "include/flutter_tts_windows/flutter_tts_windows.h" +#include -#include +#include "include/flutter_tts_windows/flutter_tts_windows.h" // This must be included before many other Windows headers. #include @@ -21,8 +21,14 @@ using namespace flutter_tts; typedef std::function reply)> FlutterResult; -#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP) && \ +#if defined(WINAPI_FAMILY) && \ + (WINAPI_FAMILY == WINAPI_FAMILY_PC_APP || \ + WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP) && \ !defined(FORCE_NON_DESKTOP) +#define USE_WINRT 1 +#endif + +#if defined(USE_WINRT) #include #include #include @@ -46,8 +52,12 @@ using namespace std::chrono_literals; #endif +const winrt::hstring kSpeakTextForSourceKey = L"SpeakingTextKey"; +const winrt::hstring kTrackIdWordBoundary = L"SpeechWord"; +const winrt::hstring kTrackIdSentenceBoundary = L"SpeechSentence"; + namespace { -class FlutterTtsPlugin : public flutter::Plugin, TtsHostApi { +class FlutterTtsPlugin : public flutter::Plugin, TtsHostApi, WinTtsHostApi { public: static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); FlutterTtsPlugin(flutter::BinaryMessenger* binary_messenger); @@ -84,9 +94,11 @@ class FlutterTtsPlugin : public flutter::Plugin, TtsHostApi { virtual void GetVoices( std::function reply)> result) override; + virtual void SetBoundaryType( + bool is_word_boundary, + std::function reply)> result) override; -#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP) && \ - !defined(FORCE_NON_DESKTOP) +#if defined(USE_WINRT) private: void speak(const std::string, FlutterResult); void pause(); @@ -100,6 +112,18 @@ class FlutterTtsPlugin : public flutter::Plugin, TtsHostApi { void getLanguages(flutter::EncodableList&); void addMplayer(); winrt::Windows::Foundation::IAsyncAction asyncSpeak(const std::string); + void registerForBoundaryEvents( + Windows::Media::Playback::MediaPlaybackItem& mediaPlaybackItem); + void timedMetadataTrackChangedHandler( + Windows::Media::Playback::MediaPlaybackItem mediaPlaybackItem, + Windows::Foundation::Collections::IVectorChangedEventArgs const& args); + void registerMetadataHandlerFor( + Windows::Media::Playback::MediaPlaybackItem& mediaPlaybackItem, + int index); + void metadataSpeechCueEntered( + const Windows::Media::Core::TimedMetadataTrack& timedMetadataTrack, + const Windows::Media::Core::MediaCueEventArgs& args); + bool speaking(); bool paused(); @@ -108,6 +132,7 @@ class FlutterTtsPlugin : public flutter::Plugin, TtsHostApi { bool isPaused; bool isSpeaking; bool awaitSpeakCompletion; + bool isWordBoundray = true; FlutterResult speakResult; TtsFlutterApi flutterApi; @@ -141,6 +166,7 @@ void FlutterTtsPlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows* registrar) { auto plugin = std::make_unique(registrar->messenger()); TtsHostApi::SetUp(registrar->messenger(), plugin.get()); + WinTtsHostApi::SetUp(registrar->messenger(), plugin.get()); registrar->AddPlugin(std::move(plugin)); } @@ -221,8 +247,17 @@ void FlutterTtsPlugin::GetVoices( result(l); } -#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP) && \ - !defined(FORCE_NON_DESKTOP) +void FlutterTtsPlugin::SetBoundaryType( + bool is_word_boundary, + std::function reply)> result) { +#if defined(USE_WINRT) + isWordBoundray = is_word_boundary; +#else +#endif + result(std::move(TtsResult(true))); +} + +#if defined(USE_WINRT) void FlutterTtsPlugin::addMplayer() { mPlayer = winrt::Windows::Media::Playback::MediaPlayer::MediaPlayer(); @@ -249,12 +284,133 @@ winrt::Windows::Foundation::IAsyncAction FlutterTtsPlugin::asyncSpeak( winrt::Windows::Media::Core::MediaSource source = winrt::Windows::Media::Core::MediaSource::CreateFromStream(speechStream, cType); - mPlayer.Source(source); + // Add the custom ID to the MediaSource's properties + // You must wrap the string in an IPropertyValue for WinRT interop + source.CustomProperties().Insert( + kSpeakTextForSourceKey, Windows::Foundation::PropertyValue::CreateString( + winrt::to_hstring(text))); + + auto item = winrt::Windows::Media::Playback::MediaPlaybackItem(source); + registerForBoundaryEvents(item); + + mPlayer.Source(item); mPlayer.Play(); } +/// +/// Register for all boundary events and register a function to add any new +/// events if they arise. +/// +/// The Media Playback Item to register events +/// for. +void FlutterTtsPlugin::registerForBoundaryEvents( + Windows::Media::Playback::MediaPlaybackItem& mediaPlaybackItem) { + // If tracks were available at source resolution time, itterate through and + // register: + auto timedMetadataTracks = mediaPlaybackItem.TimedMetadataTracks(); + auto trackSize = timedMetadataTracks.Size(); + for (unsigned int index = 0; index < trackSize; index++) { + registerMetadataHandlerFor(mediaPlaybackItem, index); + } + + // Since the tracks are added later we will  + // monitor the tracks being added and subscribe to the ones of interest + auto newHandler = winrt::Windows::Foundation::TypedEventHandler< + winrt::Windows::Media::Playback::MediaPlaybackItem, + winrt::Windows::Foundation::Collections::IVectorChangedEventArgs>( + this, &FlutterTtsPlugin::timedMetadataTrackChangedHandler); + + mediaPlaybackItem.TimedMetadataTracksChanged(newHandler); +} + +/// +/// Register for boundary events when they arise. +/// +/// The Media PLayback Item add handlers +/// to. Arguments for the event. +void FlutterTtsPlugin::timedMetadataTrackChangedHandler( + Windows::Media::Playback::MediaPlaybackItem mediaPlaybackItem, + Windows::Foundation::Collections::IVectorChangedEventArgs const& args) { + if (args.CollectionChange() == + winrt::Windows::Foundation::Collections::CollectionChange::ItemInserted) { + registerMetadataHandlerFor(mediaPlaybackItem, args.Index()); + } else if (args.CollectionChange() == + winrt::Windows::Foundation::Collections::CollectionChange::Reset) { + auto trackSize = mediaPlaybackItem.TimedMetadataTracks().Size(); + for (unsigned int index = 0; index < trackSize; index++) { + registerMetadataHandlerFor(mediaPlaybackItem, index); + } + } +} + +/// +/// Register for just word boundary events. +/// +/// The Media PLayback Item add handlers +/// to. Index of the timedMetadataTrack within the +/// mediaPlaybackItem. +void FlutterTtsPlugin::registerMetadataHandlerFor( + Windows::Media::Playback::MediaPlaybackItem& mediaPlaybackItem, int index) { + auto timedTrack = mediaPlaybackItem.TimedMetadataTracks().GetAt(index); + // register for only word cues + const auto& trackIdToCheck = + isWordBoundray ? kTrackIdWordBoundary : kTrackIdSentenceBoundary; + if (timedTrack.Id() == trackIdToCheck) { + auto handler = winrt::Windows::Foundation::TypedEventHandler< + winrt::Windows::Media::Core::TimedMetadataTrack, + winrt::Windows::Media::Core::MediaCueEventArgs>( + this, &FlutterTtsPlugin::metadataSpeechCueEntered); + timedTrack.CueEntered(handler); + + mediaPlaybackItem.TimedMetadataTracks().SetPresentationMode( + index, winrt::Windows::Media::Playback:: + TimedMetadataTrackPresentationMode::ApplicationPresented); + } +} + +/// +/// This function executes when a SpeechCue is hit and calls the functions to +/// update the UI +/// +/// The timedMetadataTrack associated with the +/// event. the arguments associated with the +/// event. +void FlutterTtsPlugin::metadataSpeechCueEntered( + const Windows::Media::Core::TimedMetadataTrack& timedMetadataTrack, + const Windows::Media::Core::MediaCueEventArgs& args) { + // Check in case there are different tracks and the handler was used for more + // tracks + auto mediaSource = timedMetadataTrack.PlaybackItem().Source(); + if (timedMetadataTrack.TimedMetadataKind() == + winrt::Windows::Media::Core::TimedMetadataKind::Speech && + mediaSource.CustomProperties().HasKey(kSpeakTextForSourceKey)) { + // Retrieve the cached text + winrt::Windows::Foundation::IInspectable speakingTextValue = + mediaSource.CustomProperties().Lookup(kSpeakTextForSourceKey); + + // Cast the IInspectable back to the original string type + auto speakingText = winrt::unbox_value(speakingTextValue); + + auto speachCue = + args.Cue().try_as(); + + auto startIndex = + static_cast(speachCue.StartPositionInInput().Value()); + auto endIndex = + static_cast(speachCue.EndPositionInInput().Value()); + endIndex = min(endIndex + 1, static_cast(speakingText.size())); + + auto progress = + flutter_tts::TtsProgress(winrt::to_string(speakingText), startIndex, + endIndex, winrt::to_string(speachCue.Text())); + flutterApi.OnSpeakProgressCb(progress, []() {}, [](const FlutterError&) {}); + } +} + void FlutterTtsPlugin::speak(const std::string text, FlutterResult result) { isSpeaking = true; + synth.Options().IncludeSentenceBoundaryMetadata(!isWordBoundray); + synth.Options().IncludeWordBoundaryMetadata(isWordBoundray); auto my_task{asyncSpeak(text)}; flutterApi.OnSpeakStartCb([]() {}, [](const FlutterError&) {}); if (awaitSpeakCompletion) @@ -303,7 +459,8 @@ void FlutterTtsPlugin::setRate(const double newRate) { void FlutterTtsPlugin::getVoices(flutter::EncodableList& voices) { auto synthVoices = synth.AllVoices(); for (auto voice : synthVoices) { - auto voiceInfo = Voice(to_string(voice.DisplayName()), to_string(voice.Language())); + auto voiceInfo = + Voice(to_string(voice.DisplayName()), to_string(voice.Language())); // Convert VoiceGender to string std::string gender; switch (voice.Gender()) { @@ -497,7 +654,7 @@ void FlutterTtsPlugin::getVoices(flutter::EncodableList& voices) { cpAttribKey->GetStringValue(L"Name", &psz); std::string name = CW2A(psz); ::CoTaskMemFree(psz); - auto voiceInfo = Voice (name, language); + auto voiceInfo = Voice(name, language); voices.push_back(flutter::CustomEncodableValue(voiceInfo)); cpVoiceToken->Release(); } diff --git a/packages/flutter_tts_windows/windows/messages.g.cpp b/packages/flutter_tts_windows/windows/messages.g.cpp index 385a9ff6..3663fc5c 100644 --- a/packages/flutter_tts_windows/windows/messages.g.cpp +++ b/packages/flutter_tts_windows/windows/messages.g.cpp @@ -1601,6 +1601,70 @@ EncodableValue MacosTtsHostApi::WrapError(const FlutterError& error) { }); } +/// The codec used by WinTtsHostApi. +const flutter::StandardMessageCodec& WinTtsHostApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance(&PigeonInternalCodecSerializer::GetInstance()); +} + +// Sets up an instance of `WinTtsHostApi` to handle messages through the `binary_messenger`. +void WinTtsHostApi::SetUp( + flutter::BinaryMessenger* binary_messenger, + WinTtsHostApi* api) { + WinTtsHostApi::SetUp(binary_messenger, api, ""); +} + +void WinTtsHostApi::SetUp( + flutter::BinaryMessenger* binary_messenger, + WinTtsHostApi* api, + const std::string& message_channel_suffix) { + const std::string prepended_suffix = message_channel_suffix.length() > 0 ? std::string(".") + message_channel_suffix : ""; + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.flutter_tts.WinTtsHostApi.setBoundaryType" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_is_word_boundary_arg = args.at(0); + if (encodable_is_word_boundary_arg.IsNull()) { + reply(WrapError("is_word_boundary_arg unexpectedly null.")); + return; + } + const auto& is_word_boundary_arg = std::get(encodable_is_word_boundary_arg); + api->SetBoundaryType(is_word_boundary_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } +} + +EncodableValue WinTtsHostApi::WrapError(std::string_view error_message) { + return EncodableValue(EncodableList{ + EncodableValue(std::string(error_message)), + EncodableValue("Error"), + EncodableValue() + }); +} + +EncodableValue WinTtsHostApi::WrapError(const FlutterError& error) { + return EncodableValue(EncodableList{ + EncodableValue(error.code()), + EncodableValue(error.message()), + error.details() + }); +} + // Generated class from Pigeon that represents Flutter messages that can be called from C++. TtsFlutterApi::TtsFlutterApi(flutter::BinaryMessenger* binary_messenger) : binary_messenger_(binary_messenger), diff --git a/packages/flutter_tts_windows/windows/messages.g.h b/packages/flutter_tts_windows/windows/messages.g.h index eb8f28d8..b40c03f4 100644 --- a/packages/flutter_tts_windows/windows/messages.g.h +++ b/packages/flutter_tts_windows/windows/messages.g.h @@ -52,6 +52,7 @@ template class ErrorOr { friend class IosTtsHostApi; friend class AndroidTtsHostApi; friend class MacosTtsHostApi; + friend class WinTtsHostApi; friend class TtsFlutterApi; ErrorOr() = default; T TakeValue() && { return std::get(std::move(v_)); } @@ -374,6 +375,7 @@ class Voice { friend class IosTtsHostApi; friend class AndroidTtsHostApi; friend class MacosTtsHostApi; + friend class WinTtsHostApi; friend class TtsFlutterApi; friend class PigeonInternalCodecSerializer; std::string name_; @@ -409,6 +411,7 @@ class TtsResult { friend class IosTtsHostApi; friend class AndroidTtsHostApi; friend class MacosTtsHostApi; + friend class WinTtsHostApi; friend class TtsFlutterApi; friend class PigeonInternalCodecSerializer; bool success_; @@ -445,6 +448,7 @@ class TtsProgress { friend class IosTtsHostApi; friend class AndroidTtsHostApi; friend class MacosTtsHostApi; + friend class WinTtsHostApi; friend class TtsFlutterApi; friend class PigeonInternalCodecSerializer; std::string text_; @@ -483,6 +487,7 @@ class TtsRateValidRange { friend class IosTtsHostApi; friend class AndroidTtsHostApi; friend class MacosTtsHostApi; + friend class WinTtsHostApi; friend class TtsFlutterApi; friend class PigeonInternalCodecSerializer; double minimum_; @@ -690,6 +695,31 @@ class MacosTtsHostApi { protected: MacosTtsHostApi() = default; }; +// Generated interface from Pigeon that represents a handler of messages from Flutter. +class WinTtsHostApi { + public: + WinTtsHostApi(const WinTtsHostApi&) = delete; + WinTtsHostApi& operator=(const WinTtsHostApi&) = delete; + virtual ~WinTtsHostApi() {} + virtual void SetBoundaryType( + bool is_word_boundary, + std::function reply)> result) = 0; + + // The codec used by WinTtsHostApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `WinTtsHostApi` to handle messages through the `binary_messenger`. + static void SetUp( + flutter::BinaryMessenger* binary_messenger, + WinTtsHostApi* api); + static void SetUp( + flutter::BinaryMessenger* binary_messenger, + WinTtsHostApi* api, + const std::string& message_channel_suffix); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + protected: + WinTtsHostApi() = default; +}; // Generated class from Pigeon that represents Flutter messages that can be called from C++. class TtsFlutterApi { public: diff --git a/pigeons/messages.dart b/pigeons/messages.dart index 735e6f13..6e5fa406 100644 --- a/pigeons/messages.dart +++ b/pigeons/messages.dart @@ -390,7 +390,7 @@ abstract class TtsHostApi { } @HostApi() -abstract class IosTtsHostApi extends TtsHostApi { +abstract class IosTtsHostApi { @async TtsResult awaitSynthCompletion(bool awaitCompletion); @@ -425,7 +425,7 @@ abstract class IosTtsHostApi extends TtsHostApi { } @HostApi() -abstract class AndroidTtsHostApi extends TtsHostApi { +abstract class AndroidTtsHostApi { @async TtsResult awaitSynthCompletion(bool awaitCompletion); @@ -475,7 +475,7 @@ abstract class AndroidTtsHostApi extends TtsHostApi { } @HostApi() -abstract class MacosTtsHostApi extends TtsHostApi { +abstract class MacosTtsHostApi { @async TtsResult awaitSynthCompletion(bool awaitCompletion); @@ -489,6 +489,12 @@ abstract class MacosTtsHostApi extends TtsHostApi { bool isLanguageAvailable(String language); } +@HostApi() +abstract class WinTtsHostApi { + @async + TtsResult setBoundaryType(bool isWordBoundary); +} + @FlutterApi() abstract class TtsFlutterApi { void onSpeakStartCb();