Flutter 音声合成

音声合成パッケージの利用

次は音声合成(text to speech)です。音声認識の時と同様に、使えそうなパッケージをhttps://pub.devから探します。検索すると、複数の候補がありますが、一つに絞らなければなりません。

選択基準は対象とするアプリの性格によって変わります。今回は、機能の豊富さよりも安定性重視で決めたいと思います。そこで、

  1. ある程度のバージョンアップを重ねているもの、
  2. 最終更新日から時間が経ちすぎていないもの、

の、2つの観点で決めます。

バージョンが若いと、潜在バグに悩まされる可能性がありますし、最終更新日から何年も経っていると、他のパッケージとの関係で使えなかったりするからです。

結果、flutter_ttsを選択しました。

flutter_ttsの使い方

flutter_ttsの使い方は、こちらにあります。まずは、以下のコマンドでインストールしておきます。

flutter pub add flutter_tts

音声認識に比べて、音声合成の実装はシンプルです。

  FlutterTts flutterTts = FlutterTts();    
  String _speakText = "今日も良い天気です";   
  
 Future<void> _speak() async {
    await flutterTts.setLanguage("ja-JP");
    await flutterTts.setSpeechRate(1.0);
    await flutterTts.setVolume(1.0);
    await flutterTts.setPitch(1.0);
    await flutterTts.speak(_speakText);
  }

例えば、ボタンをクリックしたタイミングなどで_speak()を呼び出すと、「今日も良い天気です」と、話します。

おうむ返しアプリケーション

次に、前回の音声認識のサンプルコードと組み合わせて、「おうむ返し」アプリケーションにしてみます。

音声認識のサンプルコードでは、ボタンを押すと、音声認識がスタートし、次にボタンを押すと音声認識がストップします。また、音声認識結果は認識の都度、_lastWordsにセットされ、setState()で画面に反映されていました。

今回は、音声認識が終了したタイミングで、音声合成を走らせるようにします。

改良したコードは、以下の通りです。

import 'package:flutter/material.dart';
import 'package:speech_to_text/speech_recognition_result.dart';
import 'package:speech_to_text/speech_to_text.dart';
import 'package:flutter_tts/flutter_tts.dart';       //  ①
import "dart:async";                       // ※1

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  FlutterTts flutterTts = FlutterTts();            // ②
  SpeechToText _speechToText = SpeechToText();
  bool _speechEnabled = false;
  String _lastWords = '';

  @override
  void initState() {
    super.initState();
    _initSpeech();
    _initFlutterTts();                      // ③
  }

  /// This has to happen only once per app
  void _initSpeech() async {
    _speechEnabled = await _speechToText.initialize();
    setState(() {});
  }

  void _initFlutterTts() async {               // ④
    await flutterTts.setIosAudioCategory(IosTextToSpeechAudioCategory.playback,
        [IosTextToSpeechAudioCategoryOptions.defaultToSpeaker]);  // ※ 2

    await flutterTts.setLanguage("ja-JP");
    await flutterTts.setSpeechRate(0.5);
    await flutterTts.setVolume(1.0);
    await flutterTts.setPitch(1.0);
  }

  /// Each time to start a speech recognition session
  void _startListening() async {
    await flutterTts.stop();                 // ⑤
    await _speechToText.listen(onResult: _onSpeechResult);
    setState(() {});
  }

  /// Manually stop the active speech recognition session
  /// Note that there are also timeouts that each platform enforces
  /// and the SpeechToText plugin supports setting timeouts on the
  /// listen method.
  void _stopListening() async {
    await _speechToText.stop();
    await new Future.delayed(new Duration(seconds: 1));  // ※ 3
    await _speak();                       //  ⑥
    setState(() {});
  }

  /// This is the callback that the SpeechToText plugin calls when
  /// the platform returns recognized words.
  void _onSpeechResult(SpeechRecognitionResult result) {
    setState(() {
      _lastWords = result.recognizedWords;
    });
  }

  Future<void> _speak() async {                 // ⑦
    //音声合成
    if (_lastWords != "") {
      await flutterTts.speak(_lastWords);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Speech Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              padding: EdgeInsets.all(16),
              child: Text(
                'Recognized words:',
                style: TextStyle(fontSize: 20.0),
              ),
            ),
            Expanded(
              child: Container(
                padding: EdgeInsets.all(16),
                child: Text(
                  // If listening is active show the recognized words
                  _speechToText.isListening
                      ? '$_lastWords'
                      // If listening isn't active but could be tell the user
                      // how to start it, otherwise indicate that speech
                      // recognition is not yet ready or not supported on
                      // the target device
                      : _speechEnabled
                          ? 'Tap the microphone to start listening...'
                          : 'Speech not available',
                ),
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed:
            // If not yet listening for speech start, otherwise stop
            _speechToText.isNotListening ? _startListening : _stopListening,
        tooltip: 'Listen',
        child: Icon(_speechToText.isNotListening ? Icons.mic_off : Icons.mic),
      ),
    );
  }
}

① 〜⑦の部分が音声合成のために追加したコードです。基本的には、これで動作すると思いましたが2箇所ほど修正が必要でした。

  1. iPhoneの場合ですが、電話のスピーカーから音声が出力されていました。これを修正するために、flutterTtsの初期化関数_initFlutterTts()の中で、オーディオスピーカーをセットしています。 ※ 2
  2. 音声合成が途中で途切れてしまう問題がありました。タイミングのようでしたので、音声認識終了後にディレイを1秒設けました。 ※ 1,3

これで、音声による「おうむ返しアプリケーション」の完成です。