Flutter を使ったテキスト読み上げ機能を紹介します。
TTS → text to speech と呼ばれています。
読み上げアプリの必要性
多機能 → シンプル → 読み上げ
スマホアプリは画面の小ささからシンプルさが求められました。
しかし頻繁に見なければいけないという問題があります。
シンプルな通知なら耳で行えばいいのでは。
最近はカーフ型イヤホンも普及して、眼鏡の人もイヤホンを付けられるようになりました。
読み上げアプリは黎明期です。そこに期待があるのです。
サンプルアプリ
Flutter で実装しました。
ソースコード
pubspec.yaml
flutter_tts: ^4.2.5
main.dart
import 'package:flutter/material.dart';
import '/tts_controller.dart';
void main() {
runApp(
MaterialApp(
home: TtsScreen(),
),
);
}
class TtsScreen extends StatefulWidget {
@override
TtsState createState() => TtsState();
}
class TtsState extends State<TtsScreen> {
TtsController ctrl = TtsController();
String text = """
ヒトは発声器官を通じて音声を生成し、コミュニケーションを行なう。
この音声を人工的に生成するタスクが音声合成である。合成された音声を合成音声と呼ぶ。
音声合成は様々な手法で実現できる。ある種の楽器は人の声に似た音を発し、
また人の喉を模倣した機械に風を吹き込むことで人の声に似た音が生成できる。
コンピューターを用い、音声情報処理の一種としてデジタル的に音声を合成することもできる。
""";
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
padding: EdgeInsets.fromLTRB(30, 70, 30, 20),
child: Column(
children: [
Row(children: [
IconButton(
icon: Icon(Icons.volume_up),
iconSize: 32,
onPressed: () => ctrl.startSpeaking(text),
),
SizedBox(width: 10),
IconButton(
icon: Icon(Icons.stop),
iconSize: 32,
onPressed: () => ctrl.stopSpeaking(),
),
]),
SizedBox(height: 20),
Text(text),
],
),
),
);
}
}
tts_controller.dart
import 'package:flutter_tts/flutter_tts.dart';
class TtsController {
TtsController() {}
FlutterTts flutterTts = FlutterTts();
bool isSpeaking = false;
int index = 0;
List<String> sentences = [];
Future startSpeaking(String text) async {
if (isSpeaking) return;
flutterTts.setCompletionHandler(() async {
completeSpeaking();
});
await flutterTts.setSpeechRate(0.5); // 速さ 0.5
await flutterTts.setVolume(1.0); // 音量 1.0
await flutterTts.setPitch(1.0); // ピッチ 1.0
await flutterTts.setLanguage("ja-JP");
await flutterTts.setVoice({"name": "O-ren", "locale": "ja-JP"}); // オーレン女性
// 文章を。で区切る
String s = text.replaceAll('\n', '');
sentences = s.split('。');
sentences.removeWhere((value) => value == '');
index = 0;
isSpeaking = true;
speak();
}
Future speak() async {
if (isSpeaking) {
if (index < sentences.length) {
String s = sentences[index];
await Future.delayed(Duration(milliseconds: 1000));
await flutterTts.speak(s);
} else {
isSpeaking = false;
}
}
}
// 一文終了のコールバック
completeSpeaking() {
index++;
speak();
}
Future stopSpeaking() async {
isSpeaking = false;
flutterTts.stop();
}
}
解説
読み上げ関数
読み上げ自体は超シンプルです。
await flutterTts.speak('あいうえお');
「。」で待つ処理
「。」で待つ処理は自前でやります。「。」で区切りリスト化します。一文ごとに1000ミリ秒待ってから次の文を読み上げます。
漢字の読み方
漢字の読み方に対して表示用と読み上げ用のひらがなテキストの二種類を作る必要があります。
ルビ(ふりがな)
正攻法でHTMLのRUBYを使うと処理が大変です。
獅子
<ruby><rb>獅子</rb><rp>(</rp><rt>しし</rt><rp>)</rp></ruby>
小説投稿サイトでは「 | 」「《 》」で囲むとルビになります。
↓ 投稿例
突然|獅子《しし》が吠えた。
↓ 実際の表示
突然獅子が吠えた。
自作アプリ
自作の読書アプリをリリースしています。
アプリ名「一日一冊」 iOSのみ 無料
ルビ(ふりがな)に対応しています。ひらがなだけを抽出して表示用・読み上げ用を作ります。
一度出たルビは覚える機能があります。
読んでいる文だけハイライトします。そのため文章をHTMLのIDで囲っています。
セリフ「」内は声の高さを少し上げています。
ボイス一覧サンプル
各OSに入っているボイス一覧を出力するサンプルコードです。
// ボイス一覧サンプル
Future showVoices() async {
List voices = await flutterTts.getVoices;
for (var item in voices) {
var map = item as Map<Object?, Object?>;
if (map["gender"].toString() != "unspecified") {
if (map["locale"].toString() == "ja-JP") {
print('${map["name"]} ${map["locale"]} ${map["gender"]}');
}
if (map["locale"].toString().contains("en-US")) {
print('${map["name"]} ${map["locale"]} ${map["gender"]}');
}
}
}
}
iOS の実行結果
Fred en-US male
Nicky en-US female
Aaron en-US male
Samantha en-US female
Hattori ja-JP male
Kyoko ja-JP female
O-ren ja-JP female
iOSでは男性1人女性2人の声が入っています。他にも機械の音声などがあります。
まとめ
Flutter の読み上げアプリを紹介しました。「。」で待つ処理は自前で行います。特定の漢字の読みも自前で行います。
エンジニアの仕事は新しいパイを持ってくることです。パイを奪い合うことではありません。読み上げ機能にはチャンスがあります。いつチャンスが来てもいいように準備しておきましょう。

