dlutton / flutter_tts

Flutter Text to Speech package
MIT License
617 stars 258 forks source link

setVoice() doesn't work #506

Closed ghostman2013 closed 5 months ago

ghostman2013 commented 5 months ago

🐛 Bug Report

Hello!

The setVoice() method doesn't work neither in iOS 18 Simulator, neither on a real iPhone (iOS 18). The flutter_tts always speaks by a default male voice.

Also there is a "weird" types. If you call getVoices the method will return _List<_Map<Object?, Object?>> (seems it should be List<Map<String, String>>) but if later you call setVoice() it expects Map<String, String> and fails on _Map<Object?, Object?>.

Expected behavior

The voice will be switched.

Reproduction steps

  1. Call flutterTts.getVoices and choose some suitable female voice.
  2. Convert types to prevent an application crash: final convertedVoiceMap = Map<String, String>.from(selectedVoiceMap);.
  3. Call await flutterTts.setVoice(convertedVoiceMap);.

Configuration

class TtsBloc extends Bloc<TtsEvent, TtsState> {
  FlutterTts? flutterTts;
  String locale;
  String? _engine;
  Map? _voice;

  TtsBloc({required this.flutterTts, required this.locale})
      : super(TtsInitial()) {
    on<TtsEvent>(_onTtsEvent, transformer: sequential());
  }

  @override
  Future<void> close() {
    flutterTts?.stop();
    flutterTts = null;
    return super.close();
  }

  Future<void> _onTtsEvent(TtsEvent event, Emitter<TtsState> emit) async {
    TtsState state;
    switch (event) {
      case TtsInitialized():
        await _onTtsInitialized();
        state = TtsInitial();
      case TtsFailed(:final error):
        state = TtsFailure(error: error);
      case TtsPlayed(:final text):
        if (text.isNotEmpty) {
          await _onTtsPlayed(text);
          state = TtsInProgress(text: text);
        } else {
          state = const TtsFailure(error: 'Exception: text cannot be empty.');
        }
    }
    emit(state);
  }

  Future<void> _onTtsInitialized() async {
    flutterTts = FlutterTts();
    await flutterTts?.setSharedInstance(true);
    await flutterTts?.setIosAudioCategory(IosTextToSpeechAudioCategory.playback,
        [
          IosTextToSpeechAudioCategoryOptions.allowBluetooth,
          IosTextToSpeechAudioCategoryOptions.allowBluetoothA2DP,
          IosTextToSpeechAudioCategoryOptions.mixWithOthers,
          IosTextToSpeechAudioCategoryOptions.defaultToSpeaker
        ],
        IosTextToSpeechAudioMode.defaultMode
    );

    await _setAwaitOptions();

    if (Platform.isAndroid) {
      await _getDefaultEngine();
      await _findBestVoice();
    }

    await _findBestVoice();

    flutterTts!.setStartHandler(() {});

    flutterTts!.setCompletionHandler(() {});

    flutterTts!.setCancelHandler(() {});

    flutterTts!.setPauseHandler(() {});

    flutterTts!.setContinueHandler(() {});

    flutterTts!.setErrorHandler((msg) => add(TtsFailed(error: msg.toString())));
  }

  Future<void> _setAwaitOptions() async {
    await flutterTts?.awaitSpeakCompletion(true);
  }

  Future<void> _getDefaultEngine() async {
    _engine = await flutterTts?.getDefaultEngine;
  }

  Future<void> _findBestVoice() async {
    final voices = await flutterTts?.getVoices;
    (Voice, dynamic)? bestVoice;
    for (final voiceRawData in voices) {
      final voiceData = Map<String, String>.from(voiceRawData);
      final voice = TtsUtils.toVoice(voiceData);
      if (voice == null || voice.locale != locale) continue; // Bypass.

      if (bestVoice == null) {
        bestVoice = (voice, voiceData); // Select any voice with suitable locale.
      } else {
        bestVoice = (_selectBestVoice(bestVoice.$1, voice), voiceData);
      }
    }

    if (bestVoice != null) {
      await flutterTts?.setVoice(bestVoice.$2);
    }
  }

  Future<void> _onTtsPlayed(String text) async {
    await flutterTts?.speak(text);
  }

  Voice _selectBestVoice(Voice voice1, Voice voice2) {
    // Give the priority for a female's voice.
    final genderResult = voice1.gender.compareTo(voice2.gender);
    if (genderResult > 0) return voice1;
    if (genderResult < 0) return voice2;

    final qualityResult = voice1.quality.compareTo(voice2.quality);
    if (qualityResult > 0) return voice1;
    if (qualityResult < 0) return voice2;

    return voice1; // Voice parameters are identical, return the first one.
  }
}

Version: ^4.0.2

Platform:

ghostman2013 commented 5 months ago

Well, I got... Need to pass name and locale only.