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:
parent
5cc894aff7
commit
00212bf49c
26 changed files with 18 additions and 5716 deletions
1
third_party/README.md
vendored
1
third_party/README.md
vendored
|
|
@ -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
|
||||
|
|
|
|||
493
third_party/flutter_tts/CHANGELOG.md
vendored
493
third_party/flutter_tts/CHANGELOG.md
vendored
|
|
@ -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
|
||||
21
third_party/flutter_tts/LICENSE
vendored
21
third_party/flutter_tts/LICENSE
vendored
|
|
@ -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.
|
||||
275
third_party/flutter_tts/README.md
vendored
275
third_party/flutter_tts/README.md
vendored
|
|
@ -1,275 +0,0 @@
|
|||
# Text To Speech
|
||||
|
||||
[](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).
|
||||
66
third_party/flutter_tts/android/build.gradle
vendored
66
third_party/flutter_tts/android/build.gradle
vendored
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
|
@ -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
|
||||
|
|
@ -1 +0,0 @@
|
|||
rootProject.name = 'flutter_tts'
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.eyedeadevelopment.fluttertts" />
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface FlutterTtsPlugin : NSObject<FlutterPlugin>
|
||||
@end
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
22
third_party/flutter_tts/ios/flutter_tts.podspec
vendored
22
third_party/flutter_tts/ios/flutter_tts.podspec
vendored
|
|
@ -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
|
||||
|
||||
668
third_party/flutter_tts/lib/flutter_tts.dart
vendored
668
third_party/flutter_tts/lib/flutter_tts.dart
vendored
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
265
third_party/flutter_tts/lib/flutter_tts_web.dart
vendored
265
third_party/flutter_tts/lib/flutter_tts_web.dart
vendored
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
67
third_party/flutter_tts/lib/interop_types.dart
vendored
67
third_party/flutter_tts/lib/interop_types.dart
vendored
|
|
@ -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
Loading…
Reference in a new issue