feat(learn): voice mode goes, as a spoken conversation isn't wanted (ADR 0021, D68 withdrawn)

The developer's review of 4 Nov 2026: "an actual conversation with the AI
chatbot is not desirable and necessary". The field's voice-mode button,
the screen it opened, reading answers aloud and its texts go, and with
them flutter_tts, its patched copy in third_party/ (one override fewer),
the TTS_SERVICE query in the manifest and the plugin's registrants. The
microphone stays and dictates into the question. ImageApproval is the
conversation sheet's own again, as only voice mode needed it.

Whats-New: Voice mode is gone; the microphone dictates into your question.
Whats-New-De: Der Sprachmodus ist weg; das Mikrofon diktiert in deine Frage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-04 20:38:11 +01:00
commit 00212bf49c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
26 changed files with 18 additions and 5716 deletions

View file

@ -16,7 +16,6 @@ compilerOptions { jvmTarget } }` block added with the same JVM target.
|---|---|---|---|
| `flutter_web_auth_2` | 5.1.0 | MIT | `halkyon_oidc_flutter` (signing in with SimpleLogin) |
| `keyboard_height_plugin` | 0.3.0 | BSD-3-Clause | `appflowy_editor` (Learn's notes) |
| `flutter_tts` | 4.2.5 | MIT | Learn's voice mode |
When a plugin's own release has migrated, take it from pub.dev again: drop its
override and its folder here. Before then, a newer release is brought in by copying

View file

@ -1,493 +0,0 @@
# ChangeLog
## 4.2.5
### Fixes
- **Android:** Fixing namespace path
## 4.2.4
### Fixes
- **Windows:** CMake fix
### Features
- **iOS:** Add playbackAndRecord category to iOS audio category mapping
- **Android:** Bump to AGP 8.13
## 4.2.3
### Fixes
- **Android:** Fix TTS reconnection behaviors
- **Windows:** Replace deprecated exec_program with execute_process
- **Windows:** Fix error on MSVC compilers
## 4.2.2
### Fixes
- **Web:** Fixing setLanguage on iOS browsers
## 4.2.1
### Fixes
- **iOS:** speeding up iOS plugin initialization
- **Android:** Fixes npe on plugin initialization
## 4.2.0
### Features
- **Windows:** Added extra metadata for voices
- **Web:** Added progress events using onboundary
## 4.1.0
### Fixes
- **Windows:** Fixes setvoices
- **iOS:** Fixes exported audio having the wrong sampleRate on ios 18
- **Android:** Fixes 'Reply already submitted' bug during synthesizeToFile
### Features
- **Android:** Added AudioFocus and Audio Attributes For Navigation
- **Android/iOS/macOS:** Added extra metadata for voices
- **Web:** Migrated js_interop to support WASM
- **Android/iOS:** Improving synthesizeToFile
## 4.0.2
### Fixes
- **Package:** using recommended static analysis and fixing issues from analyze
- **Package:** Upating android example to use declarative plugins block
## 4.0.1
### Fixes
- **Android:** Fixing synthesizeToFile
## 4.0.0
### Features
- **iOS:** Add clear selected voice
- **Android:** Add clear selected voice
- **Android:** Upgrade to Kotlin 1.9.10
- **Android:** Upgrade Gradle to 8.2.0
- **Android:** Change compileSDK target to 34
### Fixes
- **Android:** Fix TTS initialization (removed setInitHandler)
- **Web:** Fix pause
- **Package:** Fix describeEnum deprecation (requires Flutter SDK >=2.15.0)
- **Package:** Remove unnecessary foundation.dart include
## 3.8.5
### Fixes
- **iOS:** Fix AVSpeechSynthesisVoiceGender with available on iOS 13 or greater
## 3.8.4
- **Android:** Fix 'Reply already submitted' bug
- **iOS:** Return additional voice information fields (quality, gender, and identifier)
- **iOS:** Fix synthesizeToFile on iOS 17+
## 3.8.3
### Fixes
- **Android:** Clear pending method calls on initialization
## 3.8.2
### Fixes
- **Web:** Fixing onError to prevent crashing
## 3.8.1
- **iOS:** Fixing fileName type back to URL
## 3.8.0
### Features
- *iOS/Android:** Support relative path of synthesization audio file
- **Android:** Improved android manifest notice
- **Android:** Removed deprecated registerWith
- **iOS:** Improved iOS audio documentation
### Fixes
- **Android:** Set speaking and synth to false in the stop method
- **Web:** Fix error in check available language without locale (country)
## 3.7.0
### Features
- **iOS:** add option to disable the auto stop for the audio session after speech completion
- **Android:** Upgrade gradle and kotlin. Make project compatible with AGP 8
### Fixes
- **Web:** Add try-catch on initializing web plugin.
- **iOS:** Support project import fallback if the generated compatibility header is not copied when this plugin is created as a library.
## 3.6.3
- **Android:** fixed initHandler with setEngineInitHandler
- **Android:** refactored pause work around which fixed a couple of reports bugs
## 3.6.2
- **Android:** setEngine - adding initHandler within the method
## 3.6.1
### Fixes
- **Android:** setEngine initialization bug
### Features
- **Android:** added initHandler for android to help with engine initialization
## 3.6.0
### Features
- **Android:** Adding pause/continue
- **Windows:** Adding callbacks
### Fixes
- **All:** Adding async/await to Channel invokeMethods
## 3.5.3
### Features
- **Android:** Adding getDefaultVoice
## 3.5.2
### Updates
- **Android:** Converting Java to Kotlin and package upgrades
## 3.5.1
### Fixes
- **Web:** Fixing late variable and updating value being passed through a message channel to a primitive type
## 3.5.0
### Features
- **Web:** - Adding setVoice support
### Fixes
- **Web:** Fix chrome bug
## 3.4.0
### Features
- **Windows:** - Adding Windows Support
## 3.3.3
### Fixes
- **iOS:** Fix synthesizeToFile fatal error
## 3.3.2
### Fixes
- **Android:** Fix build.gradle missing property
## 3.3.1
### Fixes
- **Web:** Fix awaitSpeakCompletion
## 3.3.0
### Features
- **iOS:** Adding support for AVAudioSession.Mode
## 3.2.4
### Fixes
- **Android:** Fix compileSDK version
## 3.2.3
### Fixes
- **Android:** Fix await speak completion when queue mode is set to 1
## 3.2.2
### Fixes
- **Android:** Android 11 fix
## 3.2.1
### Fixes
- **Android:** Fix deprecation warning
## 3.2.0
### Features
- **Android:** Adding get max speech input length
- **Android:** Changed Android speed rate mapping
- **iOS/macOS/Android:** Adding awaitSynthCompletion for synthesize to file
### Fixes
- **Android:** Fix tts connection disconnection after long running time
## 3.1.0
### Features
- **Android:** Adding set engine and get default engine
## 3.0.0
### Features
- Adding null safety
## 2.1.0
### Features
- **Android:** Adding the ability to set the queue mode
## 2.0.0
### Features
- **Android:** Adding installed language check
### BREAKING CHANGES
- **iOS/Android:** Adding voice name and locale to voice methods
## 1.3.0
- All: Adding await speak completion
## 1.2.7
- iOS: Adding utterance settings to synthesizeToFile
- Android: Fix crash when utterance completes after Engine is detached
## 1.2.6
- Android: Fixing new API onAttachedToEngine call
## 1.2.5
- macOS: Adding support
## 1.2.4
- iOS: Deactivate audio session only when needed
- Android: Better exception handling for getLanguages
- Android: Adding getEngines
- Android: Adding new Android plugin APIs
## 1.2.3
- iOS: Adding synthesize to file
- iOS: Fix set category ios
## 1.2.2
- Web: Adding pause and continue
## 1.2.1
- iOS: Fix setting iOS Category
- iOS/Android: Adding additional platform calls
## 1.2.0
- iOS: Adding Set Audio Session Category
- Android: Bug fixes
## 1.1.0
- iOS: Adding Pause
- iOS: Adding set Shared Instance
## 1.0.0
- iOS/Android: Feature - Speech marks
- iOS/Android: Breaking changes to Speech rate
## 0.9.2
- iOS: Changing audio session to playAndRecord
## 0.9.1
- Android: Fixing method call error
## 0.9.0
- Android: Adding synthesize to file
## 0.8.7
- Android: Fix sporadic ANR on initialization
## 0.8.6
- iOS: Reducing volume of other sessions while TTS is playing
## 0.8.5
- Android: Fix Android async platform initialization
- Android: Removing initHandler
## 0.8.2
- Web: Adding isLanguageAvailable method
- All: Fixing isLanguageAvailable platform channel and making it case insensitive
## 0.8.0
- Web: Adding Web Support
## 0.7.0
- iOS: Adding Swift version 4.2 to podspec and correct audio playback category
## 0.6.0
- Android: AndroidX support
## 0.5.2
- Android: Bug Fix on isLanguageAvailable
## 0.5.1
- Applying flutter format to fix health suggestion
## 0.5.0
- Android: Adding silence before speak
- Android: Removing deprecated API
## 0.2.6
- IOS: Add voice selection implementation
## 0.2.5
- Android: Ensure invokeMethod runs on main thread
## 0.2.4
- Android: setting minSDK back to 21 and adding instructions to readme
- Android: Adding fallback for getLanguages and defaultLanguage
## 0.2.3
- IOS: Audio continues with the Ring/Silent switch set to silent
## 0.2.2
- Android: Fixing Locale bug
## 0.2.1
- IOS: Fixing getLanguages bug
## 0.2.0
- Android: Adding exception catch for samsung devices
- Android: Using default com.google.android.tts engine
- Android: Get and Set Voice
- Android: InitHandler
- Cleaning up the example
## 0.1.2
- Support for Android background execution
- Updating Android build gradle version to 3.2.1
## 0.1.1
- Fixing TTS bound error in the example
- Fixing default voice language not found error on Android
## 0.1.0
- Updating version for improved maintenance score
- Updating package description for improved maintenance score
## 0.0.8
- Adding analysis_options.yaml for improved health score
- Fixing info/errors from flutter analyze
## 0.0.7
- Updating SDK version in pubspec.yaml
- Adding package link to README
- Fixing language string warning received from xcode
## 0.0.6
- Android: Upgrading Gradle 4.1 to 4.4
- Android: Setting minSdk version to 21
- Android: Adding try/catch to getAvailableLanguages & getDefaultVoice methods (Issues with API 21 & 22)
## 0.0.5
- Adding IOS/Android isLanguageAvailable
- Rename setRate to setSpeechRate
## 0.0.4
- Simplify podspec for Cocoapods 1.5.0
## 0.0.3
- Adding IOS/Android speech pitch and volume
## 0.0.2
- Flutter formatting and fixing pubspec sdk versioning
## 0.0.1
- first POC :
- methods : speak, stop, setRate, setLangauge, getLanguages
- a globalHandler for completion

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2018 Daniel Lutton
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,275 +0,0 @@
# Text To Speech
[![pub package](https://img.shields.io/pub/v/flutter_tts.svg?style=for-the-badge&colorB=green)](https://pub.dartlang.org/packages/flutter_tts)
A flutter text to speech plugin (Swift,Kotlin)
## Features
- [x] Android, iOS, Web, Windows & macOS
- [x] speak
- [x] stop
- [x] get languages
- [x] set language
- [x] set speech rate
- [x] set speech volume
- [x] set speech pitch
- [x] get voices
- [x] set voice
- [x] Android, iOS, Web & macOS
- [x] is language available
- [x] Android, iOS, Web
- [x] speech marks (requires iOS 7+, Android 26+, and default voice engine for web)
- [x] Android, iOS
- [x] synthesize to file (requires iOS 13+)
- [x] Android, iOS, Web, & Windows
- [x] pause
- [x] Android
- [x] set silence
- [x] is language installed
- [x] are languages installed
- [x] get engines
- [x] set engine
- [x] get default engine
- [x] get default voice
- [x] set queue mode
- [x] get max speech input length
- [x] iOS
- [x] set shared instance
- [x] set audio session category
## Usage
## macOS
```bash
OSX version: 10.15
```
[Example App](https://github.com/dlutton/flutter_tts/tree/macOS_app) from the macOS_app branch
## Web
[Website](https://dlutton.github.io/flutter_tts) from the example directory.
**Progress updates on Web**
Progress updates are only supported for native speech synsthesis. Use the default engine to ensure support for progress updates. [Chromium#41195426](https://issues.chromium.org/issues/41195426#comment8)
## Android
Change the minimum Android sdk version to 21 (or higher) in your `android/app/build.gradle` file.
```groovy
minSdkVersion 21
```
**Update the Kotlin Gradle Plugin Version**
Change the verision of the Kotlin Gradle plugin to `1.9.10`. <br>
If your project was created with a version of Flutter before 3.19, go to the `android/build.gradle` file and update the `ext.kotlin_version`:
```groovy
ext.kotlin_version = '1.9.10'
```
Otherwise go to `android/settings.gradle` and update the verion of the plugin `org.jetbrains.kotlin.android`:
```groovy
id "org.jetbrains.kotlin.android" version "1.9.10" apply false
```
Apps targeting Android 11 that use text-to-speech should
declare [`TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE`](https://developer.android.com/reference/android/speech/tts/TextToSpeech.Engine#INTENT_ACTION_TTS_SERVICE)
in the `queries` elements of their manifest.
```xml
<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>
```
### Pausing on Android
Android TTS does not support the pause function natively, so we have implemented a work around. We utilize the native `onRangeStart()` method to determine the index of start when `pause` is invoked. We use that index to create a new text the next time `speak` is invoked. Due to using `onRangeStart()`, pause works on SDK versions >= 26. Also, if using `start` and `end` offsets inside of `setProgressHandler()`, you'll need to keep a track of them if using `pause` since they will update once the new text is created when `speak` is called after being paused.
```dart
await flutterTts.pause()
```
## iOS
There's a known issue with integrating plugins that use Swift into a Flutter project created with the Objective-C template. [Flutter#16049](https://github.com/flutter/flutter/issues/16049)
[Example](https://github.com/dlutton/flutter_tts/blob/master/example/lib/main.dart)
To use this plugin :
- add the dependency to your [pubspec.yaml](https://github.com/dlutton/flutter_tts/blob/master/example/pubspec.yaml) file.
```yaml
dependencies:
flutter:
sdk: flutter
flutter_tts:
```
- instantiate FlutterTts
```dart
FlutterTts flutterTts = FlutterTts();
```
To set shared audio [instance](https://developer.apple.com/documentation/avfoundation/avaudiosession/1616504-sharedinstance) (iOS only):
```dart
await flutterTts.setSharedInstance(true);
```
To set audio [category and options](https://developer.apple.com/documentation/avfoundation/avaudiosession) with optional [mode](https://developer.apple.com/documentation/avfaudio/avaudiosession/mode) (iOS only). The following setup allows background music and in-app audio session to continue simultaneously:
```dart
await flutterTts.setIosAudioCategory(IosTextToSpeechAudioCategory.ambient,
[
IosTextToSpeechAudioCategoryOptions.allowBluetooth,
IosTextToSpeechAudioCategoryOptions.allowBluetoothA2DP,
IosTextToSpeechAudioCategoryOptions.mixWithOthers
],
IosTextToSpeechAudioMode.voicePrompt
);
```
To await speak completion.
```dart
await flutterTts.awaitSpeakCompletion(true);
```
To await synthesize to file completion.
```dart
await flutterTts.awaitSynthCompletion(true);
```
### speak, stop, getLanguages, setLanguage, setSpeechRate, getVoices, setVoice, setVolume, setPitch, isLanguageAvailable, setSharedInstance
```dart
Future _speak() async{
var result = await flutterTts.speak("Hello World");
if (result == 1) setState(() => ttsState = TtsState.playing);
}
Future _stop() async{
var result = await flutterTts.stop();
if (result == 1) setState(() => ttsState = TtsState.stopped);
}
List<dynamic> languages = await flutterTts.getLanguages;
await flutterTts.setLanguage("en-US");
await flutterTts.setSpeechRate(1.0);
await flutterTts.setVolume(1.0);
await flutterTts.setPitch(1.0);
await flutterTts.isLanguageAvailable("en-US");
// iOS, Android and Web only
//see the "Pausing on Android" section for more info
await flutterTts.pause();
// iOS, macOS, and Android only
// The last parameter is an optional boolean value for isFullPath (defaults to false)
await flutterTts.synthesizeToFile("Hello World", Platform.isAndroid ? "tts.wav" : "tts.caf", false);
// Each voice is a Map containing at least these keys: name, locale
// - Windows (UWP voices) only: gender, identifier
// - iOS, macOS only: quality, gender, identifier
// - Android only: quality, latency, network_required, features
List<Map> voices = await flutterTts.getVoices;
await flutterTts.setVoice({"name": "Karen", "locale": "en-AU"});
// iOS, macOS only
await flutterTts.setVoice({"identifier": "com.apple.voice.compact.en-AU.Karen"});
// iOS only
await flutterTts.setSharedInstance(true);
// Android only
await flutterTts.speak("Hello World", focus: true);
await flutterTts.setSilence(2);
await flutterTts.getEngines;
await flutterTts.getDefaultVoice;
await flutterTts.isLanguageInstalled("en-AU");
await flutterTts.areLanguagesInstalled(["en-AU", "en-US"]);
await flutterTts.setQueueMode(1);
await flutterTts.getMaxSpeechInputLength;
await flutterTts.setAudioAttributesForNavigation();
```
### Listening for platform calls
```dart
flutterTts.setStartHandler(() {
setState(() {
ttsState = TtsState.playing;
});
});
flutterTts.setCompletionHandler(() {
setState(() {
ttsState = TtsState.stopped;
});
});
flutterTts.setProgressHandler((String text, int startOffset, int endOffset, String word) {
setState(() {
_currentWord = word;
});
});
flutterTts.setErrorHandler((msg) {
setState(() {
ttsState = TtsState.stopped;
});
});
flutterTts.setCancelHandler((msg) {
setState(() {
ttsState = TtsState.stopped;
});
});
// Android, iOS and Web
flutterTts.setPauseHandler((msg) {
setState(() {
ttsState = TtsState.paused;
});
});
flutterTts.setContinueHandler((msg) {
setState(() {
ttsState = TtsState.continued;
});
});
```
## Getting Started
For help getting started with Flutter, view our online
[documentation](https://flutter.dev/).
For help on editing plugin code, view the [documentation](https://flutter.dev/platform-plugins/#edit-code).

View file

@ -1,66 +0,0 @@
def args = ["-Xlint:deprecation"]
group 'com.eyedeadevelopment.fluttertts'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '2.2.20'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
project.getTasks().withType(JavaCompile).configureEach {
options.compilerArgs.addAll(args)
}
apply plugin: 'com.android.library'
android {
compileSdk 36
if (project.android.hasProperty("namespace")) {
namespace 'com.eyedeadevelopment.fluttertts'
}
defaultConfig {
minSdkVersion 24
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
disable 'InvalidPackage'
disable 'GradleDependency'
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
}
repositories {
mavenCentral()
}
// Halkyon's patch (4 Nov 2026): Kotlin built into the Android Gradle plugin
// (AGP 9, android.builtInKotlin=true) rather than the Kotlin Gradle plugin
// applied here, as Flutter's guide for plugin authors has it.
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
}
}

View file

@ -1,3 +0,0 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View file

@ -1,6 +0,0 @@
#Fri Sep 27 12:18:40 PDT 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip

View file

@ -1 +0,0 @@
rootProject.name = 'flutter_tts'

View file

@ -1,3 +0,0 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.eyedeadevelopment.fluttertts" />

View file

@ -1,815 +0,0 @@
package com.eyedeadevelopment.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<Runnable>()
private val utterances = HashMap<String, String>()
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<String, String?>()
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<String?, String>? = 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<String?>? = 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<String?>): Map<String?, Boolean> {
val result: MutableMap<String?, Boolean> = 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<String> = 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<String?, String>, 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<HashMap<String, String>>()
try {
for (voice in tts!!.voices) {
val voiceMap = HashMap<String, String>()
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<String>()
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<String>()
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<String, String>()
if (defaultVoice != null) {
readVoiceProperties(voice, defaultVoice)
}
result.success(voice)
}
// Add voice properties into the voice map
fun readVoiceProperties(map: MutableMap<String, String>, 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<String, String>()
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<Field> = 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)
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -1,4 +0,0 @@
#import <Flutter/Flutter.h>
@interface FlutterTtsPlugin : NSObject<FlutterPlugin>
@end

View file

@ -1,15 +0,0 @@
#import "FlutterTtsPlugin.h"
#if __has_include(<flutter_tts/flutter_tts-Swift.h>)
#import <flutter_tts/flutter_tts-Swift.h>
#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<FlutterPluginRegistrar>*)registrar {
[SwiftFlutterTtsPlugin registerWithRegistrar:registrar];
}
@end

View file

@ -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<String> = {
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<String>
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<String>?, 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"
}
}
}

View file

@ -1,22 +0,0 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'flutter_tts'
s.version = '0.0.1'
s.summary = 'A flutter text to speech plugin.'
s.description = <<-DESC
A flutter text to speech plugin
DESC
s.homepage = 'https://github.com/dlutton/flutter_tts'
s.license = { :file => '../LICENSE' }
s.author = { 'eyedeadevelopment' => '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'
s.static_framework = true
end

View file

@ -1,668 +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<dynamic> 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<dynamic> awaitSynthCompletion(bool awaitCompletion) async =>
await _channel.invokeMethod('awaitSynthCompletion', awaitCompletion);
/// [Future] which invokes the platform specific method for speaking
Future<dynamic> speak(String text, {bool focus = false}) async {
if (!kIsWeb && Platform.isAndroid) {
return await _channel.invokeMethod('speak', <String, dynamic>{
"text": text,
"focus": focus,
});
} else {
return await _channel.invokeMethod('speak', text);
}
}
/// [Future] which invokes the platform specific method for pause
Future<dynamic> pause() async => await _channel.invokeMethod('pause');
/// [Future] which invokes the platform specific method for getMaxSpeechInputLength
/// ***Android supported only***
Future<int?> get getMaxSpeechInputLength async {
return await _channel.invokeMethod<int?>('getMaxSpeechInputLength');
}
/// [Future] which invokes the platform specific method for synthesizeToFile
/// ***Android and iOS supported only***
Future<dynamic> synthesizeToFile(String text, String fileName,
[bool isFullPath = false]) async =>
_channel.invokeMethod('synthesizeToFile', <String, dynamic>{
"text": text,
"fileName": fileName,
"isFullPath": isFullPath,
});
/// [Future] which invokes the platform specific method for setLanguage
Future<dynamic> 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<dynamic> 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<dynamic> setVolume(double volume) async =>
await _channel.invokeMethod('setVolume', volume);
/// [Future] which invokes the platform specific method for shared instance
/// ***iOS supported only***
Future<dynamic> 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<dynamic> autoStopSharedSession(bool autoStop) async =>
await _channel.invokeMethod('autoStopSharedSession', autoStop);
/// [Future] which invokes the platform specific method for setting audio category
/// ***Ios supported only***
Future<dynamic> setIosAudioCategory(IosTextToSpeechAudioCategory category,
List<IosTextToSpeechAudioCategoryOptions> options,
[IosTextToSpeechAudioMode mode =
IosTextToSpeechAudioMode.defaultMode]) async {
const categoryToString = <IosTextToSpeechAudioCategory, String>{
IosTextToSpeechAudioCategory.ambientSolo: iosAudioCategoryAmbientSolo,
IosTextToSpeechAudioCategory.ambient: iosAudioCategoryAmbient,
IosTextToSpeechAudioCategory.playback: iosAudioCategoryPlayback,
IosTextToSpeechAudioCategory.playAndRecord:
iosAudioCategoryPlaybackAndRecord,
};
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, String>{
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<dynamic>('setIosAudioCategory', <String, dynamic>{
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<dynamic> 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<dynamic> 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<dynamic> setVoice(Map<String, String> voice) async =>
await _channel.invokeMethod('setVoice', voice);
/// [Future] which resets the platform voice to the default
Future<dynamic> clearVoice() async =>
await _channel.invokeMethod('clearVoice');
/// [Future] which invokes the platform specific method for stop
Future<dynamic> 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<dynamic> 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<dynamic> 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<dynamic> 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<dynamic> 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<dynamic> 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<dynamic> 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<dynamic> 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<dynamic> areLanguagesInstalled(List<String> languages) async =>
await _channel.invokeMethod('areLanguagesInstalled', languages);
Future<SpeechRateValidRange> get getSpeechRateValidRange async {
final validRange = await _channel.invokeMethod('getSpeechRateValidRange')
as Map<dynamic, dynamic>;
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<dynamic> 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<dynamic> 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<dynamic> 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<dynamic, dynamic>;
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<void> setAudioAttributesForNavigation() async {
await _channel.invokeMethod('setAudioAttributesForNavigation');
}
}

View file

@ -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<dynamic>? _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<SpeechSynthesisVoice> voices = [];
List<String> 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<String, dynamic> progressArgs = {
'text': text,
'start': charIndex,
'end': endIndex,
'word': word
};
channel.invokeMethod("speak.onProgress", progressArgs);
}.toJS;
}
Future<dynamic> 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<String, String>.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<String?, String?> 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<String?>? _getLanguages() {
if (voices.isEmpty) _setVoices();
if (languages.isEmpty) _setLanguages();
return languages;
}
void _setVoices() {
voices = synth.getVoices().toDart;
}
Future<List<Map<String, String>>> getVoices() async {
var tmpVoices = synth.getVoices().toDart;
return tmpVoices
.map((voice) => {"name": voice.name, "locale": voice.lang})
.toList();
}
void _setLanguages() {
var langs = <String>{};
for (var v in voices) {
langs.add(v.lang);
}
languages = langs.toList();
}
}

View file

@ -1,67 +0,0 @@
import 'dart:js_interop';
@JS('speechSynthesis')
external SpeechSynthesis get synth;
@JS()
extension type SpeechSynthesis._(JSObject _) implements JSObject {
external void cancel();
external JSArray<SpeechSynthesisVoice> getVoices();
external void pause();
external void resume();
external void speak(SpeechSynthesisUtterance utterance);
}
@JS()
extension type SpeechSynthesisUtterance._(JSObject _) implements JSObject {
external SpeechSynthesisUtterance();
external String lang;
external double pitch;
external double rate;
external String text;
external SpeechSynthesisVoice? voice;
external double volume;
// Event listeners
@JS('onstart')
external set onStart(JSFunction listener);
@JS('onend')
external set onEnd(JSFunction listener);
@JS('onpause')
external set onPause(JSFunction listener);
@JS('onresume')
external set onResume(JSFunction listener);
@JS('onerror')
external set onError(JSFunction listener);
@JS('onboundary')
external set onBoundary(JSFunction listener);
}
@JS()
extension type SpeechSynthesisVoice._(JSObject _) implements JSObject {
@JS('default')
external bool get isDefault;
external String get lang;
@JS('localService')
external bool get isLocalService;
external String get name;
}

Some files were not shown because too many files have changed in this diff Show more