diff --git a/.vscode/launch.json b/.vscode/launch.json index 024463d7..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/.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 deleted file mode 100644 index 24d029a9..00000000 --- a/android/build.gradle +++ /dev/null @@ -1,64 +0,0 @@ -def args = ["-Xlint:deprecation"] - -group 'com.tundralabs.fluttertts' -version '1.0-SNAPSHOT' - -buildscript { - ext.kotlin_version = '1.9.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.2.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -project.getTasks().withType(JavaCompile).configureEach { - options.compilerArgs.addAll(args) -} - -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - compileSdk 34 - if (project.android.hasProperty("namespace")) { - namespace 'com.tundralabs.fluttertts' - } - - defaultConfig { - minSdkVersion 21 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } -} -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/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml deleted file mode 100644 index a88a091e..00000000 --- a/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt b/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt deleted file mode 100644 index 36fd1421..00000000 --- a/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt +++ /dev/null @@ -1,815 +0,0 @@ -package com.tundralabs.fluttertts - -import android.content.ContentValues -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 -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 - - -/** FlutterTtsPlugin */ -class FlutterTtsPlugin : MethodCallHandler, FlutterPlugin { - private var handler: Handler? = null - private var methodChannel: MethodChannel? = null - private var speakResult: Result? = null - private var synthResult: Result? = null - private var awaitSpeakCompletion = false - private var speaking = false - private var awaitSynthCompletion = false - private var synth = false - private var context: Context? = null - private var tts: TextToSpeech? = null - private val tag = "TTS" - private val pendingMethodCalls = ArrayList() - private val utterances = HashMap() - private var bundle: Bundle? = null - private var silencems = 0 - private var lastProgress = 0 - private var currentText: String? = null - private var pauseText: String? = null - private var isPaused: Boolean = false - private var queueMode: Int = TextToSpeech.QUEUE_FLUSH - private var ttsStatus: Int? = null - private var selectedEngine: String? = null - private var engineResult: Result? = null - private var parcelFileDescriptor: ParcelFileDescriptor? = null - private var audioManager: AudioManager? = null - private var audioFocusRequest: AudioFocusRequest? = null - - companion object { - private const val SILENCE_PREFIX = "SIL_" - private const val SYNTHESIZE_TO_FILE_PREFIX = "STF_" - } - - private fun initInstance(messenger: BinaryMessenger, context: Context) { - this.context = context - methodChannel = MethodChannel(messenger, "flutter_tts") - methodChannel!!.setMethodCallHandler(this) - handler = Handler(Looper.getMainLooper()) - bundle = Bundle() - tts = TextToSpeech(context, onInitListenerWithoutCallback) - } - - /** Android Plugin APIs */ - override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - initInstance(binding.binaryMessenger, binding.applicationContext) - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - stop() - tts!!.shutdown() - 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) - } else { - if (isPaused) { - invokeMethod("speak.onContinue", true) - isPaused = false - } else { - Log.d(tag, "Utterance ID has started: $utteranceId") - invokeMethod("speak.onStart", true) - } - } - if (Build.VERSION.SDK_INT < 26) { - onProgress(utteranceId, 0, utterances[utteranceId]!!.length) - } - } - - override fun onDone(utteranceId: String) { - if (utteranceId.startsWith(SILENCE_PREFIX)) return - if (utteranceId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) { - closeParcelFileDescriptor(false) - Log.d(tag, "Utterance ID has completed: $utteranceId") - if (awaitSynthCompletion) { - synthCompletion(1) - } - invokeMethod("synth.onComplete", true) - } else { - Log.d(tag, "Utterance ID has completed: $utteranceId") - if (awaitSpeakCompletion && queueMode == TextToSpeech.QUEUE_FLUSH) { - speakCompletion(1) - } - invokeMethod("speak.onComplete", true) - } - lastProgress = 0 - pauseText = null - utterances.remove(utteranceId) - releaseAudioFocus() - } - - override fun onStop(utteranceId: String, interrupted: Boolean) { - Log.d( - tag, - "Utterance ID has been stopped: $utteranceId. Interrupted: $interrupted" - ) - if (awaitSpeakCompletion) { - speaking = false - } - if (isPaused) { - invokeMethod("speak.onPause", true) - } else { - invokeMethod("speak.onCancel", true) - } - releaseAudioFocus() - } - - 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) - } - } - - // Requires Android 26 or later - override fun onRangeStart(utteranceId: String, startAt: Int, endAt: Int, frame: Int) { - if (!utteranceId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) { - lastProgress = startAt - super.onRangeStart(utteranceId, startAt, endAt, frame) - onProgress(utteranceId, startAt, endAt) - } - } - - @Deprecated("") - 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)") - } else { - if (awaitSpeakCompletion) { - speaking = false - } - invokeMethod("speak.onError", "Error from TextToSpeech (speak)") - } - releaseAudioFocus() - } - - override fun onError(utteranceId: String, errorCode: Int) { - if (utteranceId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) { - closeParcelFileDescriptor(true) - if (awaitSynthCompletion) { - synth = false - } - invokeMethod("synth.onError", "Error from TextToSpeech (synth) - $errorCode") - } else { - if (awaitSpeakCompletion) { - speaking = false - } - invokeMethod("speak.onError", "Error from TextToSpeech (speak) - $errorCode") - } - } - } - - fun speakCompletion(success: Int) { - speaking = false - handler!!.post { - speakResult?.success(success) - speakResult = null - } - } - - fun synthCompletion(success: Int) { - synth = false - handler!!.post { - synthResult?.success(success) - synthResult = null - } - } - - private val onInitListenerWithCallback: TextToSpeech.OnInitListener = - TextToSpeech.OnInitListener { status -> - // Handle pending method calls (sent while TTS was initializing) - synchronized(this@FlutterTtsPlugin) { - ttsStatus = status - for (call in pendingMethodCalls) { - call.run() - } - pendingMethodCalls.clear() - } - - if (status == TextToSpeech.SUCCESS) { - tts!!.setOnUtteranceProgressListener(utteranceProgressListener) - try { - val locale: Locale = tts!!.defaultVoice.locale - if (isLanguageAvailable(locale)) { - tts!!.language = locale - } - } catch (e: NullPointerException) { - Log.e(tag, "getDefaultLocale: " + e.message) - } catch (e: IllegalArgumentException) { - Log.e(tag, "getDefaultLocale: " + e.message) - } - - engineResult!!.success(1) - } else { - engineResult!!.error("TtsError","Failed to initialize TextToSpeech with status: $status", null) - } - //engineResult = null - } - - private val onInitListenerWithoutCallback: TextToSpeech.OnInitListener = - TextToSpeech.OnInitListener { status -> - // Handle pending method calls (sent while TTS was initializing) - synchronized(this@FlutterTtsPlugin) { - ttsStatus = status - for (call in pendingMethodCalls) { - call.run() - } - pendingMethodCalls.clear() - } - - if (status == TextToSpeech.SUCCESS) { - tts!!.setOnUtteranceProgressListener(utteranceProgressListener) - try { - val locale: Locale = tts!!.defaultVoice.locale - if (isLanguageAvailable(locale)) { - tts!!.language = locale - } - } catch (e: NullPointerException) { - Log.e(tag, "getDefaultLocale: " + e.message) - } catch (e: IllegalArgumentException) { - Log.e(tag, "getDefaultLocale: " + e.message) - } - } else { - Log.e(tag, "Failed to initialize TextToSpeech with status: $status") - } - } - - 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) { - tts!!.setSpeechRate(rate) - } - - private fun isLanguageAvailable(locale: Locale?): Boolean { - return tts!!.isLanguageAvailable(locale) >= TextToSpeech.LANG_AVAILABLE - } - - private fun areLanguagesInstalled(languages: List): Map { - val result: MutableMap = HashMap() - for (language in languages) { - result[language] = isLanguageInstalled(language) - } - return result - } - - private fun isLanguageInstalled(language: String?): Boolean { - val locale: Locale = Locale.forLanguageTag(language!!) - if (isLanguageAvailable(locale)) { - var voiceToCheck: Voice? = null - for (v in tts!!.voices) { - if (v.locale == locale && !v.isNetworkConnectionRequired) { - voiceToCheck = v - break - } - } - if (voiceToCheck != null) { - val features: Set = voiceToCheck.features - return (!features.contains(TextToSpeech.Engine.KEY_FEATURE_NOT_INSTALLED)) - } - } - return false - } - - private fun setEngine(engine: String?, result: Result) { - 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) - } - } - - private fun setVoice(voice: HashMap, result: Result) { - for (ttsVoice in tts!!.voices) { - if (ttsVoice.name == voice["name"] && ttsVoice.locale - .toLanguageTag() == voice["locale"] - ) { - tts!!.voice = ttsVoice - result.success(1) - return - } - } - Log.d(tag, "Voice name not found: $voice") - result.success(0) - } - - private fun clearVoice(result: Result) { - tts!!.voice = tts!!.defaultVoice - result.success(1) - } - - private fun setVolume(volume: Float, result: Result) { - if (volume in (0.0f..1.0f)) { - bundle!!.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, volume) - result.success(1) - } else { - Log.d(tag, "Invalid volume $volume value - Range is from 0.0 to 1.0") - result.success(0) - } - } - - private fun setPitch(pitch: Float, result: Result) { - if (pitch in (0.5f..2.0f)) { - tts!!.setPitch(pitch) - result.success(1) - } else { - Log.d(tag, "Invalid pitch $pitch value - Range is from 0.5 to 2.0") - result.success(0) - } - } - - private fun getVoices(result: Result) { - val voices = ArrayList>() - try { - for (voice in tts!!.voices) { - val voiceMap = HashMap() - readVoiceProperties(voiceMap, voice) - voices.add(voiceMap) - } - result.success(voices) - } catch (e: NullPointerException) { - Log.d(tag, "getVoices: " + e.message) - result.success(null) - } - } - - private fun getLanguages(result: Result) { - 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()) - } - } - } - } catch (e: MissingResourceException) { - Log.d(tag, "getLanguages: " + e.message) - } catch (e: NullPointerException) { - Log.d(tag, "getLanguages: " + e.message) - } - result.success(locales) - } - - private fun getEngines(result: Result) { - val engines = ArrayList() - try { - for (engineInfo in tts!!.engines) { - engines.add(engineInfo.name) - } - } catch (e: Exception) { - Log.d(tag, "getEngines: " + e.message) - } - result.success(engines) - } - - private fun getDefaultEngine(result: Result) { - val defaultEngine: String? = tts!!.defaultEngine - result.success(defaultEngine) - } - - private fun getDefaultVoice(result: Result) { - val defaultVoice: Voice? = tts!!.defaultVoice - val voice = HashMap() - if (defaultVoice != null) { - readVoiceProperties(voice, defaultVoice) - } - 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") - - } - - // Function to map quality integer to the constant name - fun qualityToString(quality: Int): String { - return when (quality) { - Voice.QUALITY_VERY_HIGH -> "very high" - Voice.QUALITY_HIGH -> "high" - Voice.QUALITY_NORMAL -> "normal" - Voice.QUALITY_LOW -> "low" - Voice.QUALITY_VERY_LOW -> "very low" - 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) { - // 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) - } - - private fun speak(text: String, focus: Boolean): Boolean { - val uuid: String = UUID.randomUUID().toString() - utterances[uuid] = text - return if (ismServiceConnectionUsable(tts)) { - if(focus){ - requestAudioFocus() - } - - if (silencems > 0) { - tts!!.playSilentUtterance( - silencems.toLong(), - TextToSpeech.QUEUE_FLUSH, - SILENCE_PREFIX + uuid - ) - tts!!.speak(text, TextToSpeech.QUEUE_ADD, bundle, uuid) == 0 - } else { - tts!!.speak(text, queueMode, bundle, uuid) == 0 - } - } else { - ttsStatus = null - tts = TextToSpeech(context, onInitListenerWithoutCallback, selectedEngine) - false - } - } - - private fun stop() { - if (awaitSynthCompletion) synth = false - if (awaitSpeakCompletion) speaking = false - tts!!.stop() - } - - private val maxSpeechInputLength: Int - get() = TextToSpeech.getMaxSpeechInputLength() - - private fun closeParcelFileDescriptor(isError: Boolean) { - if (this.parcelFileDescriptor != null) { - if (isError) { - this.parcelFileDescriptor!!.closeWithError("Error synthesizing TTS to file") - } else { - this.parcelFileDescriptor!!.close() - } - } - } - - private fun synthesizeToFile(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 - ) - - 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!!, file!!, SYNTHESIZE_TO_FILE_PREFIX + uuid) - } - - if (result == TextToSpeech.SUCCESS) { - Log.d(tag, "Successfully created file : $fullPath") - } else { - Log.d(tag, "Failed creating file : $fullPath") - } - } - - 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) { - return false - } - val fields: Array = tts.javaClass.declaredFields - for (j in fields.indices) { - fields[j].isAccessible = true - if ("mServiceConnection" == fields[j].name && "android.speech.tts.TextToSpeech\$Connection" == fields[j].type.name) { - try { - if (fields[j][tts] == null) { - isBindConnection = false - Log.e(tag, "*******TTS -> mServiceConnection == null*******") - } - } catch (e: IllegalArgumentException) { - e.printStackTrace() - } catch (e: IllegalAccessException) { - e.printStackTrace() - } catch (e: Exception) { - e.printStackTrace() - } - } - } - return isBindConnection - } - - // Method to set AudioAttributes for navigation usage - private fun setAudioAttributesForNavigation() { - if (tts != null) { - val audioAttributes = AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build() - tts!!.setAudioAttributes(audioAttributes) - } - } - - private fun requestAudioFocus() { - 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() - audioManager?.requestAudioFocus(audioFocusRequest!!) - } else { - audioManager?.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) - } - } - - private fun releaseAudioFocus() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - audioFocusRequest?.let { audioManager?.abandonAudioFocusRequest(it) } - } else { - audioManager?.abandonAudioFocus(null) - } - } -} diff --git a/example/.gitignore b/apps/example/.gitignore similarity index 100% rename from example/.gitignore rename to apps/example/.gitignore diff --git a/apps/example/.metadata b/apps/example/.metadata new file mode 100644 index 00000000..b9fd747a --- /dev/null +++ b/apps/example/.metadata @@ -0,0 +1,30 @@ +# 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: "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/README.md b/apps/example/README.md similarity index 100% rename from example/README.md rename to apps/example/README.md diff --git a/apps/example/analysis_options.yaml b/apps/example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/apps/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/.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/apps/example/android/app/build.gradle.kts b/apps/example/android/app/build.gradle.kts new file mode 100644 index 00000000..92ff5830 --- /dev/null +++ b/apps/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/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/apps/example/android/build.gradle.kts b/apps/example/android/build.gradle.kts new file mode 100644 index 00000000..dbee657b --- /dev/null +++ b/apps/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/apps/example/android/gradle.properties b/apps/example/android/gradle.properties new file mode 100644 index 00000000..f018a618 --- /dev/null +++ b/apps/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/apps/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 69% rename from example/android/gradle/wrapper/gradle-wrapper.properties rename to apps/example/android/gradle/wrapper/gradle-wrapper.properties index 10e94ad6..cdebde04 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/apps/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/apps/example/android/settings.gradle.kts b/apps/example/android/settings.gradle.kts new file mode 100644 index 00000000..75af8a40 --- /dev/null +++ b/apps/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +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 { + 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/apps/example/ios/.gitignore b/apps/example/ios/.gitignore new file mode 100644 index 00000000..7a7f9873 --- /dev/null +++ b/apps/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*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 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/apps/example/ios/Flutter/AppFrameworkInfo.plist similarity index 86% rename from example/ios/Flutter/AppFrameworkInfo.plist rename to apps/example/ios/Flutter/AppFrameworkInfo.plist index 9b41e7d8..1dc6cf76 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/apps/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,11 +20,7 @@ ???? CFBundleVersion 1.0 - UIRequiredDeviceCapabilities - - arm64 - MinimumOSVersion - 11.0 + 13.0 diff --git a/apps/example/ios/Flutter/Debug.xcconfig b/apps/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/apps/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/apps/example/ios/Flutter/Release.xcconfig b/apps/example/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/apps/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/apps/example/ios/Podfile similarity index 94% rename from example/ios/Podfile rename to apps/example/ios/Podfile index 88359b22..620e46eb 100644 --- a/example/ios/Podfile +++ b/apps/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/apps/example/ios/Runner.xcodeproj/project.pbxproj similarity index 56% rename from example/ios/Runner.xcodeproj/project.pbxproj rename to apps/example/ios/Runner.xcodeproj/project.pbxproj index 38e2ecbc..7a9624c2 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/apps/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/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 81% rename from example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to apps/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 41adb77b..e3773d42 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/apps/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/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/apps/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 new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..7353c41e Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..6ed2d933 Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..4cd7b009 Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..fe730945 Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..321773cd Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..502f463a Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..e9f5fea2 Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..84ac32ae Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..8953cba0 Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/apps/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 new file mode 100644 index 00000000..0467bf12 Binary files /dev/null and b/apps/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ 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 87% rename from example/ios/Runner/Info.plist rename to apps/example/ios/Runner/Info.plist index 51845a36..4702e343 100644 --- a/example/ios/Runner/Info.plist +++ b/apps/example/ios/Runner/Info.plist @@ -3,7 +3,9 @@ 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/apps/example/ios/Runner/Runner-Bridging-Header.h b/apps/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/apps/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/apps/example/ios/RunnerTests/RunnerTests.swift b/apps/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/apps/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/apps/example/lib/main.dart b/apps/example/lib/main.dart new file mode 100644 index 00000000..21a321e8 --- /dev/null +++ b/apps/example/lib/main.dart @@ -0,0 +1,661 @@ +// ignore_for_file: avoid_print + +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'; +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 { + 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 + State createState() => _MyAppState(); +} + +enum TtsState { playing, stopped, paused, continued } + +class _MyAppState extends State { + late FlutterTtsPlatform flutterTts; + String? language; + String? engine; + Voice? voice; + final voices = []; + double volume = 0.5; + double pitch = 1.0; + double rate = 0.5; + bool isCurrentLanguageInstalled = false; + + String? _newVoiceText; + int? _inputLength; + final _editingController = TextEditingController(); + + TtsProgress? _speakingProgess; + + bool _isWordBoundary = true; + + TtsState ttsState = TtsState.stopped; + + bool get isPlaying => ttsState == TtsState.playing; + bool get isStopped => ttsState == TtsState.stopped; + bool get isPaused => ttsState == TtsState.paused; + bool get isContinued => ttsState == TtsState.continued; + + bool get isIOS => !kIsWeb && Platform.isIOS; + bool get isAndroid => !kIsWeb && Platform.isAndroid; + bool get isWindows => !kIsWeb && Platform.isWindows; + bool get isWeb => kIsWeb; + + @override + initState() { + super.initState(); + initTts(); + } + + dynamic initTts() { + flutterTts = FlutterTts.platform; + + _setAwaitOptions(); + + if (isAndroid) { + _getDefaultEngine(); + _getDefaultVoice(); + } + + flutterTts.onSpeakStart = () { + setState(() { + print("Playing"); + ttsState = TtsState.playing; + }); + }; + + flutterTts.onSpeakComplete = () { + setState(() { + print("Complete"); + ttsState = TtsState.stopped; + _speakingProgess = null; + }); + }; + + flutterTts.onSpeakCancel = () { + setState(() { + print("Cancel"); + ttsState = TtsState.stopped; + _speakingProgess = null; + }); + }; + + flutterTts.onSpeakPause = () { + setState(() { + print("Paused"); + ttsState = TtsState.paused; + }); + }; + + flutterTts.onSpeakResume = () { + setState(() { + print("Continued"); + ttsState = TtsState.continued; + }); + }; + + flutterTts.onSpeakError = (msg) { + print("error: $msg"); + setState(() { + ttsState = TtsState.stopped; + _speakingProgess = null; + }); + }; + + flutterTts.onSpeakProgress = (progress) { + setState(() { + _speakingProgess = progress; + }); + }; + + flutterTts.getVoices().then((value) { + value.when( + (newVoices) => setState(() { + voices.clear(); + voices.addAll(newVoices); + }), + (e) => print("Error: $e"), + ); + }); + } + + 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 []; + } + } + + return []; + } + + Future _getDefaultEngine() async { + 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 { + 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; + } + } + } + + Future _speak() async { + await flutterTts.setVolume(volume); + await flutterTts.setSpeechRate(rate); + await flutterTts.setPitch(pitch); + + if (_newVoiceText != null) { + if (_newVoiceText!.isNotEmpty) { + await flutterTts.speak(_newVoiceText!); + } + } + } + + Future _setAwaitOptions() async { + await flutterTts.awaitSpeakCompletion(awaitCompletion: true); + } + + Future _stop() async { + var result = await flutterTts.stop(); + 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(); + switch (result) { + case SuccessDart(): + if (result.success.success) { + setState(() => ttsState = TtsState.paused); + } + break; + case FailureDart(): + print("Error: ${result.error}"); + break; + } + } + + @override + void dispose() { + super.dispose(); + flutterTts.stop(); + } + + List> getEnginesDropDownMenuItems( + List engines, + ) { + var items = >[]; + for (dynamic type in engines) { + items.add( + DropdownMenuItem(value: type as String?, child: Text((type as String))), + ); + } + return items; + } + + void changedEnginesDropDownItem(String? selectedEngine) async { + 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, + ) { + var items = >[]; + for (dynamic type in languages) { + items.add( + DropdownMenuItem(value: type as String?, child: Text((type as String))), + ); + } + return items; + } + + void changedLanguageDropDownItem(String? selectedType) async { + var newIsCurrentLanguageInstalled = false; + setState(() { + language = selectedType; + voice = null; + isCurrentLanguageInstalled = newIsCurrentLanguageInstalled; + }); + } + + void _onChange(String text) { + setState(() { + _newVoiceText = text; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + themeMode: ThemeMode.system, + home: Scaffold( + appBar: AppBar(title: Text('Flutter TTS')), + body: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + children: [ + 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( + 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(), + _futureBuilder(), + Row( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [_buildGetVoiceBtn(), _buildAddTextToSpeak()], + ), + if (voices.isNotEmpty) _buildSelectVoice(), + _buildSliders(), + if (isAndroid) _getMaxSpeechInputLengthSection(), + ], + ), + ), + ), + ); + } + + 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...'); + } + }, + ); + } 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); + } + } + + 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...\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( + controller: _editingController, + maxLines: 11, + minLines: 6, + onChanged: (String value) { + _onChange(value); + }, + ), + ); + + Widget _btnSection() { + return Container( + padding: EdgeInsets.only(top: 50.0), + 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, + ), + _buildButtonColumn( + 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, + ), + ); + + Widget _languageDropDownSection(List languages) => Container( + padding: EdgeInsets.only(top: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DropdownButton( + value: language, + items: getLanguageDropDownMenuItems(languages), + onChanged: changedLanguageDropDownItem, + ), + Visibility( + visible: isAndroid, + child: Text("Is installed: $isCurrentLanguageInstalled"), + ), + ], + ), + ); + + 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), + 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() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + child: Text('Get max speech input length'), + onPressed: () async {}, + ), + Text("$_inputLength characters"), + ], + ); + } + + Widget _buildSliders() { + return Column(children: [_volume(), _pitch(), _rate()]); + } + + Widget _volume() { + return Slider( + 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; + flutterTts.setPitch(pitch); + }); + }, + min: 0.5, + max: 2.0, + divisions: 15, + label: "Pitch: ${pitch.toStringAsFixed(1)}", + activeColor: Colors.red, + ); + } + + Widget _rate() { + return Slider( + value: rate, + onChanged: (newRate) { + setState(() { + rate = newRate; + flutterTts.setSpeechRate(rate); + }); + }, + min: 0.0, + max: 2.0, + divisions: 20, + label: "Rate: ${rate.toStringAsFixed(1)}", + activeColor: Colors.green, + ); + } +} diff --git a/example/macos/.gitignore b/apps/example/macos/.gitignore similarity index 65% rename from example/macos/.gitignore rename to apps/example/macos/.gitignore index f73015c4..746adbb6 100644 --- a/example/macos/.gitignore +++ b/apps/example/macos/.gitignore @@ -1,7 +1,7 @@ # Flutter-related **/Flutter/ephemeral/ **/Pods/ -**/Flutter/GeneratedPluginRegistrant.swift # Xcode-related +**/dgph **/xcuserdata/ diff --git a/apps/example/macos/Flutter/Flutter-Debug.xcconfig b/apps/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/apps/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/apps/example/macos/Flutter/Flutter-Release.xcconfig b/apps/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/apps/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/apps/example/macos/Flutter/GeneratedPluginRegistrant.swift b/apps/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..53005524 --- /dev/null +++ b/apps/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +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 95% rename from example/macos/Podfile rename to apps/example/macos/Podfile index 9ec46f8c..ff5ddb3b 100644 --- a/example/macos/Podfile +++ b/apps/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/apps/example/macos/Runner.xcodeproj/project.pbxproj similarity index 69% rename from example/macos/Runner.xcodeproj/project.pbxproj rename to apps/example/macos/Runner.xcodeproj/project.pbxproj index 7529558d..7a842fc2 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/apps/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/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 85% rename from example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to apps/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 6111bd0a..a1c30175 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/apps/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.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 62% rename from example/macos/Runner/AppDelegate.swift rename to apps/example/macos/Runner/AppDelegate.swift index d53ef643..b3c17614 100644 --- a/example/macos/Runner/AppDelegate.swift +++ b/apps/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/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/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 00000000..82b6f9d9 Binary files /dev/null and b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 00000000..13b35eba Binary files /dev/null and b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..0a3f5fa4 Binary files /dev/null and b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 00000000..bdb57226 Binary files /dev/null and b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..f083318e Binary files /dev/null and b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 00000000..326c0e72 Binary files /dev/null and b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..2f1632cf Binary files /dev/null and b/apps/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/apps/example/macos/Runner/Base.lproj/MainMenu.xib similarity index 98% rename from example/macos/Runner/Base.lproj/MainMenu.xib rename to apps/example/macos/Runner/Base.lproj/MainMenu.xib index 537341ab..80e867a4 100644 --- a/example/macos/Runner/Base.lproj/MainMenu.xib +++ b/apps/example/macos/Runner/Base.lproj/MainMenu.xib @@ -323,6 +323,10 @@ + + + + diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/apps/example/macos/Runner/Configs/AppInfo.xcconfig similarity index 68% rename from example/macos/Runner/Configs/AppInfo.xcconfig rename to apps/example/macos/Runner/Configs/AppInfo.xcconfig index f960fd48..41f79de2 100644 --- a/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/apps/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/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 84% rename from example/macos/Runner/MainFlutterWindow.swift rename to apps/example/macos/Runner/MainFlutterWindow.swift index 2722837e..3cc05eb2 100644 --- a/example/macos/Runner/MainFlutterWindow.swift +++ b/apps/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/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/apps/example/macos/RunnerTests/RunnerTests.swift b/apps/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/apps/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/apps/example/pubspec.yaml similarity index 87% rename from example/pubspec.yaml rename to apps/example/pubspec.yaml index 1f16ca43..a50cf577 100644 --- a/example/pubspec.yaml +++ b/apps/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 +resolution: workspace + +environment: + sdk: ^3.9.0 dependencies: flutter: @@ -8,22 +14,20 @@ 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: ^5.0.0 + flutter_tts_android: ^5.0.0 + flutter_tts_windows: ^5.0.0 dev_dependencies: flutter_test: sdk: flutter + flutter_lints: ^6.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 +61,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/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/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/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/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/gradle.properties b/example/android/gradle.properties deleted file mode 100644 index 022f0d32..00000000 --- a/example/android/gradle.properties +++ /dev/null @@ -1,6 +0,0 @@ -android.defaults.buildfeatures.buildconfig=true -android.enableJetifier=true -android.nonFinalResIds=false -android.nonTransitiveRClass=false -android.useAndroidX=true -org.gradle.jvmargs=-Xmx1536M 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_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 deleted file mode 100644 index 1e1aafd6..00000000 --- a/example/ios/.gitignore +++ /dev/null @@ -1,42 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!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/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba11..00000000 --- a/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#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 deleted file mode 100644 index 399e9340..00000000 --- a/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" 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 deleted file mode 100644 index 3d43d11e..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ 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 deleted file mode 100644 index 28c6bf03..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd96..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ 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 deleted file mode 100644 index 4cde1211..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd96..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ 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 deleted file mode 100644 index a6d6b860..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ 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 deleted file mode 100644 index a6d6b860..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ 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 deleted file mode 100644 index 75b2d164..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ 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 deleted file mode 100644 index c4df70d3..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f585..00000000 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 7335fdf9..00000000 --- a/example/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart deleted file mode 100644 index ae748393..00000000 --- a/example/lib/main.dart +++ /dev/null @@ -1,382 +0,0 @@ -import 'dart:async'; -import 'dart:io' show Platform; - -import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:flutter/material.dart'; -import 'package:flutter_tts/flutter_tts.dart'; - -void main() => runApp(MyApp()); - -class MyApp extends StatefulWidget { - @override - _MyAppState createState() => _MyAppState(); -} - -enum TtsState { playing, stopped, paused, continued } - -class _MyAppState extends State { - late FlutterTts flutterTts; - String? language; - String? engine; - double volume = 0.5; - double pitch = 1.0; - double rate = 0.5; - bool isCurrentLanguageInstalled = false; - - String? _newVoiceText; - int? _inputLength; - - TtsState ttsState = TtsState.stopped; - - bool get isPlaying => ttsState == TtsState.playing; - bool get isStopped => ttsState == TtsState.stopped; - bool get isPaused => ttsState == TtsState.paused; - bool get isContinued => ttsState == TtsState.continued; - - bool get isIOS => !kIsWeb && Platform.isIOS; - bool get isAndroid => !kIsWeb && Platform.isAndroid; - bool get isWindows => !kIsWeb && Platform.isWindows; - bool get isWeb => kIsWeb; - - @override - initState() { - super.initState(); - initTts(); - } - - dynamic initTts() { - flutterTts = FlutterTts(); - - _setAwaitOptions(); - - if (isAndroid) { - _getDefaultEngine(); - _getDefaultVoice(); - } - - flutterTts.setStartHandler(() { - setState(() { - print("Playing"); - ttsState = TtsState.playing; - }); - }); - - flutterTts.setCompletionHandler(() { - setState(() { - print("Complete"); - ttsState = TtsState.stopped; - }); - }); - - flutterTts.setCancelHandler(() { - setState(() { - print("Cancel"); - ttsState = TtsState.stopped; - }); - }); - - flutterTts.setPauseHandler(() { - setState(() { - print("Paused"); - ttsState = TtsState.paused; - }); - }); - - flutterTts.setContinueHandler(() { - setState(() { - print("Continued"); - ttsState = TtsState.continued; - }); - }); - - flutterTts.setErrorHandler((msg) { - setState(() { - print("error: $msg"); - ttsState = TtsState.stopped; - }); - }); - } - - Future _getLanguages() async => await flutterTts.getLanguages; - - Future _getEngines() async => await flutterTts.getEngines; - - Future _getDefaultEngine() async { - var engine = await flutterTts.getDefaultEngine; - if (engine != null) { - print(engine); - } - } - - Future _getDefaultVoice() async { - var voice = await flutterTts.getDefaultVoice; - if (voice != null) { - print(voice); - } - } - - Future _speak() async { - await flutterTts.setVolume(volume); - await flutterTts.setSpeechRate(rate); - await flutterTts.setPitch(pitch); - - if (_newVoiceText != null) { - if (_newVoiceText!.isNotEmpty) { - await flutterTts.speak(_newVoiceText!); - } - } - } - - Future _setAwaitOptions() async { - await flutterTts.awaitSpeakCompletion(true); - } - - Future _stop() async { - var result = await flutterTts.stop(); - if (result == 1) setState(() => ttsState = TtsState.stopped); - } - - Future _pause() async { - var result = await flutterTts.pause(); - if (result == 1) setState(() => ttsState = TtsState.paused); - } - - @override - void dispose() { - super.dispose(); - flutterTts.stop(); - } - - List> getEnginesDropDownMenuItems( - List engines) { - var items = >[]; - for (dynamic type in engines) { - 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; - }); - } - - List> getLanguageDropDownMenuItems( - List languages) { - var items = >[]; - for (dynamic type in languages) { - items.add(DropdownMenuItem( - value: type as String?, child: Text((type as String)))); - } - return items; - } - - void changedLanguageDropDownItem(String? selectedType) { - setState(() { - language = selectedType; - flutterTts.setLanguage(language!); - if (isAndroid) { - flutterTts - .isLanguageInstalled(language!) - .then((value) => isCurrentLanguageInstalled = (value as bool)); - } - }); - } - - void _onChange(String text) { - setState(() { - _newVoiceText = text; - }); - } - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: Text('Flutter TTS'), - ), - body: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - children: [ - _inputSection(), - _btnSection(), - _engineSection(), - _futureBuilder(), - _buildSliders(), - if (isAndroid) _getMaxSpeechInputLengthSection(), - ], - ), - ), - ), - ); - } - - 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...'); - } - }); - } else { - return Container(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...'); - }); - - 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); - }, - )); - - Widget _btnSection() { - return Container( - padding: EdgeInsets.only(top: 50.0), - 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), - _buildButtonColumn( - 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, - ), - ); - - Widget _languageDropDownSection(List languages) => Container( - padding: EdgeInsets.only(top: 10.0), - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - DropdownButton( - value: language, - items: getLanguageDropDownMenuItems(languages), - onChanged: changedLanguageDropDownItem, - ), - Visibility( - visible: isAndroid, - child: Text("Is installed: $isCurrentLanguageInstalled"), - ), - ])); - - 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), - 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() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton( - child: Text('Get max speech input length'), - onPressed: () async { - _inputLength = await flutterTts.getMaxSpeechInputLength; - setState(() {}); - }, - ), - Text("$_inputLength characters"), - ], - ); - } - - Widget _buildSliders() { - 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)}"); - } - - Widget _pitch() { - return Slider( - value: pitch, - onChanged: (newPitch) { - setState(() => pitch = newPitch); - }, - min: 0.5, - max: 2.0, - divisions: 15, - label: "Pitch: ${pitch.toStringAsFixed(1)}", - activeColor: Colors.red, - ); - } - - Widget _rate() { - return Slider( - value: rate, - onChanged: (newRate) { - setState(() => rate = newRate); - }, - min: 0.0, - max: 1.0, - divisions: 10, - label: "Rate: ${rate.toStringAsFixed(1)}", - activeColor: Colors.green, - ); - } -} diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 785633d3..00000000 --- a/example/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#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 deleted file mode 100644 index 5fba960c..00000000 --- a/example/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" 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 deleted file mode 100644 index 3c4935a7..00000000 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ 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 deleted file mode 100644 index ed4cc164..00000000 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 483be613..00000000 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ 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 deleted file mode 100644 index bcbf36df..00000000 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index 9c0a6528..00000000 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ 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 deleted file mode 100644 index e71a7261..00000000 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 8a31fe2d..00000000 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/ios/Classes/AudioCategory.swift b/ios/Classes/AudioCategory.swift deleted file mode 100644 index 74e8ea0b..00000000 --- a/ios/Classes/AudioCategory.swift +++ /dev/null @@ -1,21 +0,0 @@ -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 - } - } -} diff --git a/ios/Classes/AudioCategoryOptions.swift b/ios/Classes/AudioCategoryOptions.swift deleted file mode 100644 index f593bfdb..00000000 --- a/ios/Classes/AudioCategoryOptions.swift +++ /dev/null @@ -1,42 +0,0 @@ -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 - } - } -} diff --git a/ios/Classes/AudioModes.swift b/ios/Classes/AudioModes.swift deleted file mode 100644 index e2e30716..00000000 --- a/ios/Classes/AudioModes.swift +++ /dev/null @@ -1,63 +0,0 @@ -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 - } - 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 deleted file mode 100644 index 7b18a353..00000000 --- a/ios/Classes/SwiftFlutterTtsPlugin.swift +++ /dev/null @@ -1,506 +0,0 @@ -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 - } else { - result(1) - } - } 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 - } - 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 { - 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) - } - } else { - try audioSession.setCategory(category, options: options) - } - 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 - } - 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) - } - -} - -extension AVSpeechSynthesisVoiceQuality { - var stringValue: String { - switch self { - case .default: - return "default" - case .premium: - return "premium" - case .enhanced: - return "enhanced" - } - } -} - -@available(iOS 13.0, *) -extension AVSpeechSynthesisVoiceGender { - var stringValue: String { - switch self { - case .male: - return "male" - case .female: - return "female" - case .unspecified: - return "unspecified" - } - } -} diff --git a/lib/flutter_tts.dart b/lib/flutter_tts.dart deleted file mode 100644 index 0d2d2f2a..00000000 --- a/lib/flutter_tts.dart +++ /dev/null @@ -1,666 +0,0 @@ -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'); - } -} diff --git a/lib/flutter_tts_web.dart b/lib/flutter_tts_web.dart deleted file mode 100644 index abff5ad3..00000000 --- a/lib/flutter_tts_web.dart +++ /dev/null @@ -1,265 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; - -import 'package:flutter/services.dart'; -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; - -import 'interop_types.dart'; - -enum TtsState { playing, stopped, paused, continued } - -class FlutterTtsPlugin { - static const String platformChannel = "flutter_tts"; - static late MethodChannel channel; - bool awaitSpeakCompletion = false; - - TtsState ttsState = TtsState.stopped; - - Completer? _speechCompleter; - - bool get isPlaying => ttsState == TtsState.playing; - - bool get isStopped => ttsState == TtsState.stopped; - - bool get isPaused => ttsState == TtsState.paused; - - 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() { - try { - utterance = SpeechSynthesisUtterance(); - _listeners(); - supported = true; - } catch (e) { - print('Initialization of TTS failed. Functions are disabled. Error: $e'); - } - } - - void _listeners() { - utterance.onStart = (JSAny e) { - ttsState = TtsState.playing; - channel.invokeMethod("speak.onStart", null); - var bLocal = (utterance.voice?.isLocalService ?? false); - if (!bLocal) { - t = Timer.periodic(Duration(seconds: 14), (t) { - if (ttsState == TtsState.playing) { - synth.pause(); - synth.resume(); - } else { - t.cancel(); - } - }); - } - }.toJS; - // js.JsFunction.withThis((e) { - // ttsState = TtsState.playing; - // channel.invokeMethod("speak.onStart", null); - // }); - utterance.onEnd = (JSAny e) { - ttsState = TtsState.stopped; - if (_speechCompleter != null) { - _speechCompleter?.complete(); - _speechCompleter = null; - } - t?.cancel(); - channel.invokeMethod("speak.onComplete", null); - }.toJS; - - utterance.onPause = (JSAny e) { - ttsState = TtsState.paused; - channel.invokeMethod("speak.onPause", null); - }.toJS; - - utterance.onResume = (JSAny e) { - ttsState = TtsState.continued; - channel.invokeMethod("speak.onContinue", null); - }.toJS; - - utterance.onError = (JSObject event) { - ttsState = TtsState.stopped; - if (_speechCompleter != null) { - _speechCompleter = null; - } - t?.cancel(); - print(event); // Log the entire event object to get more details - channel.invokeMethod("speak.onError", event["error"]); - }.toJS; - - utterance.onBoundary = (JSObject event) { - int charIndex = event['charIndex'] as int; - String name = event['name'] as String; - if (name == 'sentence') return; - String text = utterance['text'] as String; - int endIndex = charIndex; - while (endIndex < text.length && - !RegExp(r'[\s,.!?]').hasMatch(text[endIndex])) { - endIndex++; - } - String word = text.substring(charIndex, endIndex); - Map progressArgs = { - 'text': text, - 'start': charIndex, - 'end': endIndex, - 'word': word - }; - channel.invokeMethod("speak.onProgress", progressArgs); - }.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) { - utterance.text = text; - if (ttsState == TtsState.paused) { - synth.resume(); - } else { - synth.speak(utterance); - } - } - } - - void _stop() { - if (ttsState != TtsState.stopped) { - synth.cancel(); - } - } - - void _pause() { - 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 _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) { - var tmpVoices = synth.getVoices().toDart; - var targetList = tmpVoices.where((e) { - 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(); - for (var lang in languages) { - if (!language!.contains('-')) { - lang = lang.split('-').first; - } - if (lang.toLowerCase() == language.toLowerCase()) return true; - } - return false; - } - - List? _getLanguages() { - if (voices.isEmpty) _setVoices(); - if (languages.isEmpty) _setLanguages(); - return languages; - } - - void _setVoices() { - voices = synth.getVoices().toDart; - } - - Future>> getVoices() async { - var tmpVoices = synth.getVoices().toDart; - return tmpVoices - .map((voice) => {"name": voice.name, "locale": voice.lang}) - .toList(); - } - - void _setLanguages() { - var langs = {}; - for (var v in voices) { - langs.add(v.lang); - } - - languages = langs.toList(); - } -} diff --git a/macos/Classes/FlutterTtsPlugin.swift b/macos/Classes/FlutterTtsPlugin.swift deleted file mode 100644 index f30bca60..00000000 --- a/macos/Classes/FlutterTtsPlugin.swift +++ /dev/null @@ -1,411 +0,0 @@ -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 - } else { - result(1) - } - } 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 - } - 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 { - 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 - if #available(macOS 10.15, *) { - voiceDict["gender"] = voice.gender.stringValue - } - 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) - } - -} - -extension AVSpeechSynthesisVoiceQuality { - var stringValue: String { - switch self { - case .default: - return "default" - case .premium: - return "premium" - case .enhanced: - return "enhanced" - } - } -} - -@available(macOS 10.15, *) -extension AVSpeechSynthesisVoiceGender { - var stringValue: String { - switch self { - case .male: - return "male" - case .female: - return "female" - case .unspecified: - return "unspecified" - } - } -} 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/packages/flutter_tts_android/android/build.gradle b/packages/flutter_tts_android/android/build.gradle new file mode 100644 index 00000000..1850753a --- /dev/null +++ b/packages/flutter_tts_android/android/build.gradle @@ -0,0 +1,61 @@ +group = "com.tundralabs.fluttertts" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "2.1.0" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.9.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "m.tundralabs.fluttertts" + + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + } + + defaultConfig { + minSdk = 24 + } + + dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_tts_android/android/gradle/wrapper/gradle-wrapper.properties similarity index 69% rename from android/gradle/wrapper/gradle-wrapper.properties rename to packages/flutter_tts_android/android/gradle/wrapper/gradle-wrapper.properties index 4310b9d6..fc176a8e 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/flutter_tts_android/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/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/packages/flutter_tts_android/android/src/main/AndroidManifest.xml b/packages/flutter_tts_android/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..21c699c7 --- /dev/null +++ b/packages/flutter_tts_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt b/packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt new file mode 100644 index 00000000..44f92023 --- /dev/null +++ b/packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/FlutterTtsPlugin.kt @@ -0,0 +1,1110 @@ +package com.tundralabs.fluttertts + +import android.content.ContentValues +import android.content.Context +import android.media.AudioAttributes +import android.media.AudioFocusRequest +import android.media.AudioManager +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.os.Handler +import android.os.Looper +import android.os.ParcelFileDescriptor +import android.provider.MediaStore +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 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 : FlutterPlugin, TtsHostApi, AndroidTtsHostApi { + private val kTtsInitTimeOutMs: Long = 1000 + + private var handler: Handler? = null + private var speakResult: ResultCallback? = null + private var synthResult: ResultCallback? = null + private var awaitSpeakCompletion = false + private var speaking = false + private var awaitSynthCompletion = false + private var synth = false + private var context: Context? = null + private var tts: TextToSpeech? = null + private val tag = "TTS" + private val pendingMethodCalls = ArrayList() + private val utterances = HashMap() + private var bundle: Bundle? = null + private var silenceMs = 0 + private var lastProgress = 0 + private var currentText: String? = null + private var pauseText: String? = null + private var isPaused: Boolean = false + private var queueMode: Int = TextToSpeech.QUEUE_FLUSH + private var ttsStatus: Int? = null + private var selectedEngine: String? = 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(context: Context) { + this.context = context + handler = Handler(Looper.getMainLooper()) + bundle = Bundle() + + handler?.postDelayed(onInitTimeoutRunnable, kTtsInitTimeOutMs) + tts = TextToSpeech(context, onInitListenerWithoutCallback) + } + + /** Android Plugin APIs */ + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + flutterApi = TtsFlutterApi(binding.binaryMessenger) + initInstance(binding.applicationContext) + TtsHostApi.setUp(binding.binaryMessenger, this) + AndroidTtsHostApi.setUp(binding.binaryMessenger, this) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + stopImpl() + tts!!.shutdown() + TtsHostApi.setUp(binding.binaryMessenger, null) + AndroidTtsHostApi.setUp(binding.binaryMessenger, null) + context = null + } + + private val utteranceProgressListener: UtteranceProgressListener = + object : UtteranceProgressListener() { + override fun onStart(utteranceId: String) { + if (utteranceId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) { + handler?.post { + flutterApi?.onSynthStartCb { } + } + } else { + if (isPaused) { + handler?.post { + flutterApi?.onSpeakResumeCb { } + } + isPaused = false + } else { + Log.d(tag, "Utterance ID has started: $utteranceId") + handler?.post { + flutterApi?.onSpeakStartCb { } + } + } + } + if (Build.VERSION.SDK_INT < 26) { + onProgress(utteranceId, 0, utterances[utteranceId]!!.length) + } + } + + override fun onDone(utteranceId: String) { + if (utteranceId.startsWith(SILENCE_PREFIX)) return + if (utteranceId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) { + closeParcelFileDescriptor(false) + Log.d(tag, "Utterance ID has completed: $utteranceId") + if (awaitSynthCompletion) { + synthCompletion(1) + } + + handler?.post { + flutterApi?.onSynthCompleteCb { } + } + } else { + Log.d(tag, "Utterance ID has completed: $utteranceId") + if (awaitSpeakCompletion && queueMode == TextToSpeech.QUEUE_FLUSH) { + speakCompletion(1) + } + + handler?.post { + flutterApi?.onSpeakCompleteCb { } + } + } + lastProgress = 0 + pauseText = null + utterances.remove(utteranceId) + releaseAudioFocus() + } + + override fun onStop(utteranceId: String, interrupted: Boolean) { + Log.d( + tag, "Utterance ID has been stopped: $utteranceId. Interrupted: $interrupted" + ) + if (awaitSpeakCompletion) { + speaking = false + } + if (isPaused) { + handler?.post { + flutterApi?.onSpeakPauseCb { } + } + } else { + handler?.post { + flutterApi?.onSpeakCancelCb { } + } + } + releaseAudioFocus() + } + + private fun onProgress(utteranceId: String?, startAt: Int, endAt: Int) { + if (utteranceId != null && !utteranceId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) { + val text = utterances[utteranceId] + if (text != null) { + var data = TtsProgress( + text = text, + start = startAt.toLong(), + end = endAt.toLong(), + word = text.substring(startAt, endAt) + ) + + handler?.post { + flutterApi?.onSpeakProgressCb(data) { } + } + } + } + } + + // Requires Android 26 or later + override fun onRangeStart(utteranceId: String, startAt: Int, endAt: Int, frame: Int) { + if (!utteranceId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) { + lastProgress = startAt + super.onRangeStart(utteranceId, startAt, endAt, frame) + onProgress(utteranceId, startAt, endAt) + } + } + + @Deprecated("Deprecated in Java") + override fun onError(utteranceId: String) { + if (utteranceId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) { + closeParcelFileDescriptor(true) + if (awaitSynthCompletion) { + synth = false + } + + handler?.post { + flutterApi?.onSynthErrorCb("Error from TextToSpeech (synth)") {} + } + } else { + if (awaitSpeakCompletion) { + speaking = false + } + + handler?.post { + flutterApi?.onSpeakErrorCb("Error from TextToSpeech (speak)") {} + } + } + releaseAudioFocus() + } + + override fun onError(utteranceId: String, errorCode: Int) { + if (utteranceId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) { + closeParcelFileDescriptor(true) + if (awaitSynthCompletion) { + synth = false + } + + handler?.post { + flutterApi?.onSynthErrorCb("Error from TextToSpeech (synth) - $errorCode") {} + } + } else { + if (awaitSpeakCompletion) { + speaking = false + } + handler?.post { + flutterApi?.onSpeakErrorCb("Error from TextToSpeech (speak) - $errorCode") {} + } + } + } + } + + fun speakCompletion(success: Int) { + speaking = false + handler!!.post { + speakResult?.invoke(Result.success(TtsResult(success != 0))) + speakResult = null + } + } + + fun synthCompletion(success: Int) { + synth = false + handler!!.post { + 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 + for (call in pendingMethodCalls) { + call.run() + } + pendingMethodCalls.clear() + } + + if (status == TextToSpeech.SUCCESS) { + tts!!.setOnUtteranceProgressListener(utteranceProgressListener) + try { + val locale: Locale = tts!!.defaultVoice.locale + if (isLanguageAvailableImpl(locale)) { + tts!!.language = locale + } + } catch (e: NullPointerException) { + Log.e(tag, "getDefaultLocale: " + e.message) + } catch (e: IllegalArgumentException) { + Log.e(tag, "getDefaultLocale: " + e.message) + } + + engineResult?.invoke(Result.success(TtsResult(true))) + } else { + 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 + for (call in pendingMethodCalls) { + call.run() + } + pendingMethodCalls.clear() + } + + if (status == TextToSpeech.SUCCESS) { + tts!!.setOnUtteranceProgressListener(utteranceProgressListener) + try { + val locale: Locale = tts!!.defaultVoice.locale + if (isLanguageAvailableImpl(locale)) { + tts!!.language = locale + } + } catch (e: NullPointerException) { + Log.e(tag, "getDefaultLocale: " + e.message) + } catch (e: IllegalArgumentException) { + Log.e(tag, "getDefaultLocale: " + e.message) + } + } else { + Log.e(tag, "Failed to initialize TextToSpeech with status: $status") + } + } + + private fun setSpeechRateImpl(rate: Float) { + tts!!.setSpeechRate(rate) + } + + private fun isLanguageAvailableImpl(locale: Locale?): Boolean { + return tts!!.isLanguageAvailable(locale) >= TextToSpeech.LANG_AVAILABLE + } + + private fun areLanguagesInstalledImpl(languages: List): Map { + val result: MutableMap = HashMap() + for (language in languages) { + result[language] = isLanguageInstalledImpl(language) + } + return result + } + + private fun isLanguageInstalledImpl(language: String?): Boolean { + val locale: Locale = Locale.forLanguageTag(language!!) + if (isLanguageAvailableImpl(locale)) { + var voiceToCheck: Voice? = null + for (v in tts!!.voices) { + if (v.locale == locale && !v.isNetworkConnectionRequired) { + voiceToCheck = v + break + } + } + if (voiceToCheck != null) { + val features: Set = voiceToCheck.features + return (!features.contains(TextToSpeech.Engine.KEY_FEATURE_NOT_INSTALLED)) + } + } + return false + } + + private fun setEngineImpl(engine: String?, result: ResultCallback) { + ttsStatus = null + selectedEngine = engine + engineResult = result + + handler?.postDelayed(onInitTimeoutRunnable, kTtsInitTimeOutMs) + tts = TextToSpeech(context, onInitListenerWithCallback, engine) + } + + 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) { + tts!!.voice = ttsVoice + callback(Result.success(TtsResult(true))) + return + } + } + Log.d(tag, "Voice name not found: $voice") + callback(Result.success(TtsResult(false))) + } + + private fun clearVoiceImpl(callback: ResultCallback) { + tts!!.voice = tts!!.defaultVoice + callback(Result.success(TtsResult(true))) + } + + private fun setVolumeImpl(volume: Float, callback: ResultCallback) { + if (volume in (0.0f..1.0f)) { + bundle!!.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, volume) + callback(Result.success(TtsResult(true))) + } else { + Log.d(tag, "Invalid volume $volume value - Range is from 0.0 to 1.0") + callback(Result.success(TtsResult(false))) + } + } + + private fun setPitchImpl(pitch: Float, callback: ResultCallback) { + if (pitch in (0.5f..2.0f)) { + tts!!.setPitch(pitch) + callback(Result.success(TtsResult(true))) + } else { + Log.d(tag, "Invalid pitch $pitch value - Range is from 0.5 to 2.0") + callback(Result.success(TtsResult(false))) + } + } + + private fun getVoicesImpl(result: ResultCallback>) { + val voices = ArrayList() + try { + for (voice in tts!!.voices) { + voices.add(readVoiceProperties(voice)) + } + result(Result.success(voices)) + } catch (e: NullPointerException) { + Log.d(tag, "getVoices: " + e.message) + result(Result.success(voices)) + } + } + + private fun getLanguagesImpl(result: ResultCallback>) { + val locales = ArrayList() + try { + // 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(Result.success(locales)) + } + + private fun getEnginesImpl(result: ResultCallback>) { + val engines = ArrayList() + try { + for (engineInfo in tts!!.engines) { + engines.add(engineInfo.name) + } + } catch (e: Exception) { + Log.d(tag, "getEngines: " + e.message) + } + result(Result.success(engines)) + } + + private fun getDefaultEngineImpl(result: ResultCallback) { + val defaultEngine: String? = tts!!.defaultEngine + result(Result.success(defaultEngine)) + } + + private fun getDefaultVoiceImpl(result: ResultCallback) { + val defaultVoice: Voice? = tts!!.defaultVoice + var voice: com.tundralabs.fluttertts.Voice? = null + if (defaultVoice != null) { + voice = readVoiceProperties(defaultVoice) + } + result(Result.success(voice)) + } + + // Add voice properties into the voice map + 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 + fun qualityToString(quality: Int): String { + return when (quality) { + Voice.QUALITY_VERY_HIGH -> "very high" + Voice.QUALITY_HIGH -> "high" + Voice.QUALITY_NORMAL -> "normal" + Voice.QUALITY_LOW -> "low" + Voice.QUALITY_VERY_LOW -> "very low" + else -> "unknown" + } + } + + 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 = TtsRateValidRange( + 0.0, 0.5, 1.5, TtsPlatform.ANDROID + ) + result(Result.success(data)) + } + + private fun speakImpl(text: String, focus: Boolean): Boolean { + val uuid: String = UUID.randomUUID().toString() + utterances[uuid] = text + return if (ismServiceConnectionUsable(tts)) { + if (focus) { + requestAudioFocus() + } + + if (silenceMs > 0) { + tts!!.playSilentUtterance( + silenceMs.toLong(), TextToSpeech.QUEUE_FLUSH, SILENCE_PREFIX + uuid + ) + tts!!.speak(text, TextToSpeech.QUEUE_ADD, bundle, uuid) == 0 + } else { + tts!!.speak(text, queueMode, bundle, uuid) == 0 + } + } else { + ttsStatus = null + handler?.postDelayed(onInitTimeoutRunnable, kTtsInitTimeOutMs) + tts = TextToSpeech(context, onInitListenerWithoutCallback, selectedEngine) + false + } + } + + private fun stopImpl() { + if (awaitSynthCompletion) synth = false + if (awaitSpeakCompletion) speaking = false + tts!!.stop() + } + + private val maxSpeechInputLength: Int + get() = TextToSpeech.getMaxSpeechInputLength() + + private fun closeParcelFileDescriptor(isError: Boolean) { + if (this.parcelFileDescriptor != null) { + if (isError) { + this.parcelFileDescriptor!!.closeWithError("Error synthesizing TTS to file") + } else { + this.parcelFileDescriptor!!.close() + } + } + } + + 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 + ) + + 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!!, file, SYNTHESIZE_TO_FILE_PREFIX + uuid) + } + + if (result == TextToSpeech.SUCCESS) { + Log.d(tag, "Successfully created file : $fullPath") + } else { + Log.d(tag, "Failed creating file : $fullPath") + } + } + + private fun ismServiceConnectionUsable(tts: TextToSpeech?): Boolean { + var isBindConnection = true + if (tts == null) { + return false + } + val fields: Array = tts.javaClass.declaredFields + for (j in fields.indices) { + fields[j].isAccessible = true + if ("mServiceConnection" == fields[j].name && "android.speech.tts.TextToSpeech\$Connection" == fields[j].type.name) { + try { + if (fields[j][tts] == null) { + isBindConnection = false + Log.e(tag, "*******TTS -> mServiceConnection == null*******") + } + } catch (e: IllegalArgumentException) { + e.printStackTrace() + } catch (e: IllegalAccessException) { + e.printStackTrace() + } catch (e: Exception) { + e.printStackTrace() + } + } + } + return isBindConnection + } + + // Method to set AudioAttributes for navigation usage + private fun setAudioAttributesForNavigationImpl() { + if (tts != null) { + val audioAttributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH).build() + tts!!.setAudioAttributes(audioAttributes) + } + } + + private fun requestAudioFocus() { + 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() + audioManager?.requestAudioFocus(audioFocusRequest!!) + } else { + audioManager?.requestAudioFocus( + null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + ) + } + } + + private fun releaseAudioFocus() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + audioFocusRequest?.let { audioManager?.abandonAudioFocusRequest(it) } + } else { + 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/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 new file mode 100644 index 00000000..edb2fdb7 --- /dev/null +++ b/packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt @@ -0,0 +1,1706 @@ +// 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 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 { + /** 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/packages/flutter_tts_android/lib/flutter_tts_android.dart b/packages/flutter_tts_android/lib/flutter_tts_android.dart new file mode 100644 index 00000000..76d3d72c --- /dev/null +++ b/packages/flutter_tts_android/lib/flutter_tts_android.dart @@ -0,0 +1,191 @@ +import 'package:flutter_tts_platform_interface/flutter_tts_platform_interface.dart'; + +/// 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(); + + /// [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), + ); + } 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 { + ensureSetupTtsCallback(); + 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. + 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 + 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 + Future> setAudioAttributesForNavigation() async { + try { + return ResultDart.success( + 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/packages/flutter_tts_ios/ios/Classes/AudioCategory.swift b/packages/flutter_tts_ios/ios/Classes/AudioCategory.swift new file mode 100644 index 00000000..367c2662 --- /dev/null +++ b/packages/flutter_tts_ios/ios/Classes/AudioCategory.swift @@ -0,0 +1,16 @@ +import AVFoundation + +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/packages/flutter_tts_ios/ios/Classes/AudioCategoryOptions.swift b/packages/flutter_tts_ios/ios/Classes/AudioCategoryOptions.swift new file mode 100644 index 00000000..fa3e7e3e --- /dev/null +++ b/packages/flutter_tts_ios/ios/Classes/AudioCategoryOptions.swift @@ -0,0 +1,30 @@ +import AVFoundation + +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/packages/flutter_tts_ios/ios/Classes/AudioModes.swift b/packages/flutter_tts_ios/ios/Classes/AudioModes.swift new file mode 100644 index 00000000..721f64fd --- /dev/null +++ b/packages/flutter_tts_ios/ios/Classes/AudioModes.swift @@ -0,0 +1,53 @@ +import AVFoundation + +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 + } + } +} diff --git a/packages/flutter_tts_ios/ios/Classes/SwiftFlutterTtsPlugin.swift b/packages/flutter_tts_ios/ios/Classes/SwiftFlutterTtsPlugin.swift new file mode 100644 index 00000000..98c7ad98 --- /dev/null +++ b/packages/flutter_tts_ios/ios/Classes/SwiftFlutterTtsPlugin.swift @@ -0,0 +1,547 @@ +import AVFoundation +import Flutter +import UIKit + +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 { + 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))) + } + } + + private func setPitchImpl(pitch: Float, completion: ResultCallback) { + if volume >= 0.5 && volume <= 2.0 { + self.pitch = pitch + completion(Result.success(TtsResult(success: true))) + } else { + 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 { + try audioSession.setCategory(category, options: options) + } + completion(Result.success(TtsResult(success: true))) + } catch { + print(error) + 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(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 { + completion(Result.failure(PigeonError(code: FlutterTtsErrorCode.notSupportedOSVersion.toStrCode(), + message: kVoiceSelectionNotSuported, + details: nil))) + } + } + + 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 + } + 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 { + var stringValue: String { + switch self { + case .default: + return "default" + case .premium: + return "premium" + case .enhanced: + return "enhanced" + } + } +} + +@available(iOS 13.0, *) +extension AVSpeechSynthesisVoiceGender { + var stringValue: String { + switch self { + case .male: + return "male" + case .female: + return "female" + case .unspecified: + return "unspecified" + } + } +} diff --git a/packages/flutter_tts_ios/ios/Classes/message.g.swift b/packages/flutter_tts_ios/ios/Classes/message.g.swift new file mode 100644 index 00000000..d429d040 --- /dev/null +++ b/packages/flutter_tts_ios/ios/Classes/message.g.swift @@ -0,0 +1,1578 @@ +// 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 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) + 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/packages/flutter_tts_ios/ios/flutter_tts_ios.podspec similarity index 89% rename from ios/flutter_tts.podspec rename to packages/flutter_tts_ios/ios/flutter_tts_ios.podspec index 03099f45..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 @@ -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/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/packages/flutter_tts_macos/macos/Classes/FlutterTtsPlugin.swift b/packages/flutter_tts_macos/macos/Classes/FlutterTtsPlugin.swift new file mode 100644 index 00000000..715f07c7 --- /dev/null +++ b/packages/flutter_tts_macos/macos/Classes/FlutterTtsPlugin.swift @@ -0,0 +1,466 @@ +import AVFoundation +import FlutterMacOS +import Foundation + +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 { + completion(Result.success(TtsResult(success: true))) + } + } + + 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))) + } + } + + private func setPitchImpl(pitch: Float, completion: ResultCallback) { + if volume >= 0.5 && volume <= 2.0 { + self.pitch = pitch + completion(Result.success(TtsResult(success: true))) + } else { + 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) + } + completion(Result.success(voices)) + } else { + completion(Result.failure(PigeonError(code: FlutterTtsErrorCode.notSupportedOSVersion.toStrCode(), + message: kVoiceSelectionNotSuported, + details: nil))) + } + } + + private func setVoiceImpl(voice: Voice, completion: ResultCallback) { + if #available(macOS 10.15, *) { + // 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))) + } + 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 { + var stringValue: String { + switch self { + case .default: + return "default" + case .premium: + return "premium" + case .enhanced: + return "enhanced" + default: + return "unknown" + } + } +} + +@available(macOS 10.15, *) +extension AVSpeechSynthesisVoiceGender { + var stringValue: String { + switch self { + case .male: + return "male" + case .female: + return "female" + case .unspecified: + return "unspecified" + default: + return "unknown" + } + } +} diff --git a/packages/flutter_tts_macos/macos/Classes/message.g.swift b/packages/flutter_tts_macos/macos/Classes/message.g.swift new file mode 100644 index 00000000..d429d040 --- /dev/null +++ b/packages/flutter_tts_macos/macos/Classes/message.g.swift @@ -0,0 +1,1578 @@ +// 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 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) + 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/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/packages/flutter_tts_platform_interface/lib/src/flutter_tts_mixin.dart b/packages/flutter_tts_platform_interface/lib/src/flutter_tts_mixin.dart new file mode 100644 index 00000000..e66a6630 --- /dev/null +++ b/packages/flutter_tts_platform_interface/lib/src/flutter_tts_mixin.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_tts_platform_interface/flutter_tts_platform_interface.dart'; +import 'package:multiple_result/multiple_result.dart'; + +/// 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(); + + 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({ + required 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 { + ensureSetupTtsCallback(); + 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/packages/flutter_tts_platform_interface/lib/src/messages.g.dart b/packages/flutter_tts_platform_interface/lib/src/messages.g.dart new file mode 100644 index 00000000..678c2cc5 --- /dev/null +++ b/packages/flutter_tts_platform_interface/lib/src/messages.g.dart @@ -0,0 +1,1959 @@ +// 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?)!; + } + } +} + +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(); + + 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/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/packages/flutter_tts_web/lib/flutter_tts_web.dart b/packages/flutter_tts_web/lib/flutter_tts_web.dart new file mode 100644 index 00000000..20dcaaf9 --- /dev/null +++ b/packages/flutter_tts_web/lib/flutter_tts_web.dart @@ -0,0 +1,296 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +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'; + +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(); + } + + /// Returns whether the TTS engine is currently playing. + bool get isPlaying => _ttsState == _TtsState.playing; + + /// Returns whether the TTS engine is currently stopped. + bool get isStopped => _ttsState == _TtsState.stopped; + + /// Returns whether the TTS engine is currently paused. + bool get isPaused => _ttsState == _TtsState.paused; + + /// Returns whether the TTS engine is currently continued. + bool get isContinued => _ttsState == _TtsState.continued; + + /// Returns whether the TTS engine is supported on the current platform. + bool supported = false; + + bool _isAwaitSpeakCompletion = false; + + _TtsState _ttsState = _TtsState.stopped; + + Completer? _speechCompleter; + + late final SpeechSynthesisUtterance _utterance; + List _voices = []; + List _languages = []; + Timer? _timer; + + @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 { + _utterance.pitch = pitch; + return ResultDart.success(TtsResult(success: true)); + } + + @override + Future> setSpeechRate(double rate) async { + _utterance.rate = 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 { + _utterance.volume = 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)); + } + + /// Await the completion of the current speech. + @override + Future> awaitSpeakCompletion({ + required 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); + } + } + + /// 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; + onSpeakStart?.call(); + final bLocal = _utterance.voice?.isLocalService ?? false; + if (!bLocal) { + _timer = Timer.periodic(const Duration(seconds: 14), (t) { + if (_ttsState == _TtsState.playing) { + synth + ..pause() + ..resume(); + } else { + t.cancel(); + } + }); + } + }.toJS; + // js.JsFunction.withThis((e) { + // ttsState = TtsState.playing; + // channel.invokeMethod("speak.onStart", null); + // }); + _utterance.onEnd = (JSAny e) { + _ttsState = _TtsState.stopped; + if (_speechCompleter != null) { + _speechCompleter?.complete(TtsResult(success: true)); + _speechCompleter = null; + } + _timer?.cancel(); + onSpeakComplete?.call(); + }.toJS; + + _utterance.onPause = (JSAny e) { + _ttsState = _TtsState.paused; + onSpeakPause?.call(); + }.toJS; + + _utterance.onResume = (JSAny e) { + _ttsState = _TtsState.continued; + onSpeakResume?.call(); + }.toJS; + + _utterance.onError = (JSObject event) { + _ttsState = _TtsState.stopped; + if (_speechCompleter != null) { + _speechCompleter = null; + } + _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()); + }.toJS; + + _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; + + /// 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++; + } + final word = text.substring(charIndex, endIndex); + final progress = TtsProgress( + text: text, + start: charIndex, + end: endIndex, + word: word, + ); + onSpeakProgress?.call(progress); + }.toJS; + } + + void _speak(String? text) { + if (text == null || text.isEmpty) return; + if (_ttsState == _TtsState.stopped || _ttsState == _TtsState.paused) { + _utterance.text = text; + if (_ttsState == _TtsState.paused) { + synth.resume(); + } else { + synth.speak(_utterance); + } + } + } + + void _stop() { + if (_ttsState != _TtsState.stopped) { + synth.cancel(); + } + } + + void _pause() { + if (_ttsState == _TtsState.playing || _ttsState == _TtsState.continued) { + synth.pause(); + } + } + + void _setVoice(Voice voice) { + 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; + } + } + + bool _isLanguageAvailable(String? language) { + if (_voices.isEmpty) _updateVoices(); + if (_languages.isEmpty) _updateLanguages(); + for (var lang in _languages) { + if (!language!.contains('-')) { + lang = lang.split('-').first; + } + if (lang.toLowerCase() == language.toLowerCase()) return true; + } + return false; + } + + List? _getLanguages() { + if (_voices.isEmpty) _updateVoices(); + if (_languages.isEmpty) _updateLanguages(); + return _languages; + } + + Future> _getVoices() async { + _updateVoices(); + return _voices + .map((voice) => Voice(name: voice.name, locale: voice.lang)) + .toList(); + } + + void _updateVoices() { + _voices = synth.getVoices().toDart; + } + + void _updateLanguages() { + final langs = {}; + for (final v in _voices) { + langs.add(v.lang); + } + + _languages = langs.toList(); + } +} diff --git a/lib/interop_types.dart b/packages/flutter_tts_web/lib/flutter_tts_web_interop_types.dart similarity index 75% rename from lib/interop_types.dart rename to packages/flutter_tts_web/lib/flutter_tts_web_interop_types.dart index 431a7a10..b6707cf4 100644 --- a/lib/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/example/.metadata b/packages/flutter_tts_windows/.metadata similarity index 69% rename from example/.metadata rename to packages/flutter_tts_windows/.metadata index 3c0dd2c8..8c15ad72 100644 --- a/example/.metadata +++ b/packages/flutter_tts_windows/.metadata @@ -4,5 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: b397406561f5e7a9c94e28f58d9e49fca0dd58b7 - channel: beta + 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..c3faf60a --- /dev/null +++ b/packages/flutter_tts_windows/lib/flutter_tts_windows.dart @@ -0,0 +1,25 @@ +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(); + } + + 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/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 86% rename from windows/CMakeLists.txt rename to packages/flutter_tts_windows/windows/CMakeLists.txt index 81148d51..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 ################ @@ -15,15 +15,17 @@ 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 # not be changed -set(PLUGIN_NAME "flutter_tts_plugin") +set(PLUGIN_NAME "${PROJECT_NAME}_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/packages/flutter_tts_windows/windows/flutter_tts_plugin.cpp b/packages/flutter_tts_windows/windows/flutter_tts_plugin.cpp new file mode 100644 index 00000000..a1c29abd --- /dev/null +++ b/packages/flutter_tts_windows/windows/flutter_tts_plugin.cpp @@ -0,0 +1,758 @@ +#include + +#include "include/flutter_tts_windows/flutter_tts_windows.h" +// This must be included before many other Windows headers. + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "messages.g.h" + +using namespace flutter_tts; + +// #define FORCE_NON_DESKTOP + +typedef std::function reply)> FlutterResult; + +#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 +using namespace winrt; +using namespace Windows::Media::SpeechSynthesis; +using namespace Concurrency; +using namespace std::chrono_literals; +#include +#include + +#else + +#include +#include + +#include +#include +#pragma warning(disable : 4996) +#include +#pragma warning(default : 4996) + +#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, WinTtsHostApi { + 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; + virtual void SetBoundaryType( + bool is_word_boundary, + std::function reply)> result) override; + +#if defined(USE_WINRT) + 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); + 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(); + + SpeechSynthesizer synth; + winrt::Windows::Media::Playback::MediaPlayer mPlayer; + bool isPaused; + bool isSpeaking; + bool awaitSpeakCompletion; + bool isWordBoundray = true; + FlutterResult speakResult; + TtsFlutterApi flutterApi; + +#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 +}; + +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)); +} + +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); +} + +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(); + 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))); + } + 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); + // 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) + 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 FlutterTtsWindowsRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + FlutterTtsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} 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/packages/flutter_tts_windows/windows/messages.g.cpp b/packages/flutter_tts_windows/windows/messages.g.cpp new file mode 100644 index 00000000..3663fc5c --- /dev/null +++ b/packages/flutter_tts_windows/windows/messages.g.cpp @@ -0,0 +1,1912 @@ +// 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() + }); +} + +/// 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), + 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/packages/flutter_tts_windows/windows/messages.g.h b/packages/flutter_tts_windows/windows/messages.g.h new file mode 100644 index 00000000..b40c03f4 --- /dev/null +++ b/packages/flutter_tts_windows/windows/messages.g.h @@ -0,0 +1,770 @@ +// 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 WinTtsHostApi; + 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 WinTtsHostApi; + 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 WinTtsHostApi; + 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 WinTtsHostApi; + 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 WinTtsHostApi; + 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 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: + 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_ diff --git a/pigeons/messages.dart b/pigeons/messages.dart new file mode 100644 index 00000000..6e5fa406 --- /dev/null +++ b/pigeons/messages.dart @@ -0,0 +1,519 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'packages/flutter_tts_platform_interface/lib/src/messages.g.dart', + dartOptions: DartOptions(), + 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: + 'packages/flutter_tts_android/android/src/main/kotlin/com/tundralabs/fluttertts/messages.g.kt', + kotlinOptions: KotlinOptions(package: "com.tundralabs.fluttertts"), + swiftOut: "packages/flutter_tts_macos/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 { + @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 { + @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 { + @async + TtsResult awaitSynthCompletion(bool awaitCompletion); + + @async + TtsRateValidRange getSpeechRateValidRange(); + + @async + TtsResult setLanguange(String language); + + @async + bool isLanguageAvailable(String language); +} + +@HostApi() +abstract class WinTtsHostApi { + @async + TtsResult setBoundaryType(bool isWordBoundary); +} + +@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..95b43f3d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,35 +1,20 @@ -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 -homepage: https://github.com/dlutton/flutter_tts - -dependencies: - flutter: - sdk: flutter - flutter_web_plugins: - sdk: flutter - -flutter: - plugin: - platforms: - android: - package: com.tundralabs.fluttertts - pluginClass: FlutterTtsPlugin - ios: - pluginClass: FlutterTtsPlugin - macos: - pluginClass: FlutterTtsPlugin - windows: - pluginClass: FlutterTtsPlugin - supportedVariants: - - uwp - - win32 - web: - pluginClass: FlutterTtsPlugin - fileName: flutter_tts_web.dart - +name: flutter_tts_workspace environment: - sdk: ">=3.4.0 <4.0.0" - flutter: ">=1.22.0" + sdk: ">=3.9.0 <4.0.0" dev_dependencies: - lints: ^3.0.0 + melos: ^7.3.0 + path: ^1.9.1 + 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 new file mode 100644 index 00000000..78def4ff --- /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, 'packages', 'flutter_tts_ios', 'ios', 'Classes'), + ); + if (!iosDir.existsSync()) { + print('iOS dir not exists: ${iosDir.path}'); + return; + } + + // 为iOS单独生成Swift代码(由于pigeon可能不直接支持同时为多个平台生成Swift) + // 这里通过手动复制生成的Swift文件到iOS目录 + final macosSwiftFile = File( + 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) { + 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/flutter_tts_plugin.cpp b/windows/flutter_tts_plugin.cpp deleted file mode 100644 index 6d59088a..00000000 --- a/windows/flutter_tts_plugin.cpp +++ /dev/null @@ -1,659 +0,0 @@ -#include "include/flutter_tts/flutter_tts_plugin.h" -// This must be included before many other Windows headers. -#include -#include -#include -#include -#include -#include -#include -#include -#include - -typedef std::unique_ptr> FlutterResult; -//typedef flutter::MethodResult* PFlutterResult; - -std::unique_ptr> methodChannel; - -#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP) -#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())); - } - -#else -#include -#include -#include -#include -#pragma warning(disable:4996) -#include -#pragma warning(default: 4996) -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&); - - 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); - } - - void CALLBACK onCompletion(PVOID lpParam, BOOLEAN TimerOrWaitFired) - { - methodChannel->InvokeMethod("speak.onComplete", NULL); - } - - 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())); - } -#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); - } - 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(); - } - } -} - -void FlutterTtsPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar) { - FlutterTtsPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarManager::GetInstance() - ->GetRegistrar(registrar)); -}