diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 8d9c8a3f..046fd486 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Sep 27 12:18:40 PDT 2019 +#Mon Aug 23 14:56:56 CST 2021 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/android/src/main/java/com/tundralabs/fluttertts/FlutterTtsPlugin.java b/android/src/main/java/com/tundralabs/fluttertts/FlutterTtsPlugin.java index 817ed2ed..0b7db99c 100644 --- a/android/src/main/java/com/tundralabs/fluttertts/FlutterTtsPlugin.java +++ b/android/src/main/java/com/tundralabs/fluttertts/FlutterTtsPlugin.java @@ -475,6 +475,10 @@ void getVoices(Result result) { HashMap voiceMap = new HashMap<>(); voiceMap.put("name", voice.getName()); voiceMap.put("locale", voice.getLocale().toLanguageTag()); + + String gender = voice.getName().contains("female") ? "female" + : voice.getName().contains("male") ? "male" : "unspecified"; + voiceMap.put("gender", gender); voices.add(voiceMap); } result.success(voices); diff --git a/example/android/.settings/org.eclipse.buildship.core.prefs b/example/android/.settings/org.eclipse.buildship.core.prefs index 9f105273..75b290b0 100644 --- a/example/android/.settings/org.eclipse.buildship.core.prefs +++ b/example/android/.settings/org.eclipse.buildship.core.prefs @@ -1,2 +1,13 @@ -#Sat Apr 14 11:55:40 PDT 2018 +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(7.0-rc-1)) connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home=/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true diff --git a/example/android/app/.classpath b/example/android/app/.classpath index 8d8d85f1..4a04201c 100644 --- a/example/android/app/.classpath +++ b/example/android/app/.classpath @@ -1,6 +1,6 @@ - + - + diff --git a/ios/Classes/SwiftFlutterTtsPlugin.swift b/ios/Classes/SwiftFlutterTtsPlugin.swift index 0e36019a..904b5a36 100644 --- a/ios/Classes/SwiftFlutterTtsPlugin.swift +++ b/ios/Classes/SwiftFlutterTtsPlugin.swift @@ -318,6 +318,15 @@ public class SwiftFlutterTtsPlugin: NSObject, FlutterPlugin, AVSpeechSynthesizer for voice in AVSpeechSynthesisVoice.speechVoices() { voiceDict["name"] = voice.name voiceDict["locale"] = voice.language + + if #available(iOS 13.0, *) { + let gender = voice.gender == AVSpeechSynthesisVoiceGender.female ? "female" + : voice.gender == AVSpeechSynthesisVoiceGender.male ? "male" : "unspecified" + voiceDict["gender"] = gender + } else { + voiceDict["gender"] = "unspecified" + } + voices.add(voiceDict) } result(voices) diff --git a/lib/flutter_tts.dart b/lib/flutter_tts.dart index 1ccbebcb..47ae6921 100644 --- a/lib/flutter_tts.dart +++ b/lib/flutter_tts.dart @@ -3,6 +3,9 @@ import 'dart:io' show Platform; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_tts/models/models.dart'; + +export 'package:flutter_tts/models/models.dart'; typedef void ErrorHandler(dynamic message); typedef ProgressHandler = void Function( @@ -210,8 +213,8 @@ class FlutterTts { /// [Future] which invokes the platform specific method for setVoice /// ***Android, iOS, and macOS supported only*** - Future setVoice(Map voice) async => - _channel.invokeMethod('setVoice', voice); + Future setVoice(TTSVoice voice) async => + _channel.invokeMethod('setVoice', voice.asVoiceMap); /// [Future] which invokes the platform specific method for stop Future stop() async => _channel.invokeMethod('stop'); @@ -241,10 +244,16 @@ class FlutterTts { } /// [Future] which invokes the platform specific method for getVoices - /// Returns a `List` of `Maps` containing a voice name and locale + /// Returns a `List` of [TTSVoice] containing a voice object /// ***Android, iOS, and macOS supported only*** - Future get getVoices async { - final voices = await _channel.invokeMethod('getVoices'); + Future> get getVoices async { + final voicesObjs = await _channel.invokeMethod('getVoices') as List; + // Parse voices to class. + final voices = voicesObjs.map((Object? voiceObj) { + final voiceMap = voiceObj as Map?; + + return TTSVoice.fromMap(voiceMap!); + }).toList(); return voices; } diff --git a/lib/models/models.dart b/lib/models/models.dart new file mode 100644 index 00000000..7a306cd6 --- /dev/null +++ b/lib/models/models.dart @@ -0,0 +1 @@ +export 'tts_voice.dart'; diff --git a/lib/models/tts_voice.dart b/lib/models/tts_voice.dart new file mode 100644 index 00000000..f15766f1 --- /dev/null +++ b/lib/models/tts_voice.dart @@ -0,0 +1,67 @@ +/// {@template tts_voice} +/// Represents a wapper around Android's Voice class and +/// Swift's AVSpeechSynthesisVoice class. +/// +/// It exposes basic information about the underlying classes like the name, +/// locale and gender of the synthetic voice. +/// +/// {@endtemplate} +class TTSVoice { + /// {@macro tts_voice} + const TTSVoice({ + required this.name, + required this.locale, + required this.gender, + }); + + TTSVoice.fromMap(Map map) + : this.name = map['name']!.toString(), + this.locale = map['locale']!.toString(), + this.gender = TTSVoiceGenderFromString.fromString( + map['gender']!.toString(), + ); + + /// The tts_voice's name. + final String name; + + /// The tts_voice's locale. + final String locale; + + /// The tts_voice's gender. + final TTSVoiceGender gender; + + Map get asVoiceMap => { + 'name': name, + 'locale': locale, + }; +} + +enum TTSVoiceGender { + male, + female, + unspecified, +} + +extension TTSVoiceGenderFromString on TTSVoiceGender { + static TTSVoiceGender fromString(String value) { + switch (value) { + case "female": + return TTSVoiceGender.female; + + case "male": + return TTSVoiceGender.male; + + default: + return TTSVoiceGender.unspecified; + } + } + + /// Helper to determine if value is male. + bool get isMale => this == TTSVoiceGender.male; + + /// Helper to determine if value is female. + bool get isFemale => this == TTSVoiceGender.female; + + /// Helper to determine if value is unspecified. + bool get isUnspecified => this == TTSVoiceGender.unspecified; +} diff --git a/macos/Classes/FlutterTtsPlugin.swift b/macos/Classes/FlutterTtsPlugin.swift index 0c5af1ad..e456833b 100644 --- a/macos/Classes/FlutterTtsPlugin.swift +++ b/macos/Classes/FlutterTtsPlugin.swift @@ -263,6 +263,13 @@ public class FlutterTtsPlugin: NSObject, FlutterPlugin, AVSpeechSynthesizerDeleg for voice in AVSpeechSynthesisVoice.speechVoices() { voiceDict["name"] = voice.name voiceDict["locale"] = voice.language + if #available(iOS 13.0, *) { + let gender = voice.gender == AVSpeechSynthesisVoiceGender.female ? "female" + : voice.gender == AVSpeechSynthesisVoiceGender.male ? "male" : "unspecified" + voiceDict["gender"] = gender + } else { + voiceDict["gender"] = "unspecified" + } voices.add(voiceDict) } result(voices)